1 题型分类
2 贪心算法
2.1 算法概念
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择
贪心算法一般按如下步骤进行:
①建立数学模型来描述问题。
②把求解的问题分成若干个子问题。
③对每个子问题求解,得到子问题的局部最优解。
④把子问题的解局部最优解合成原来解问题的一个解。
2.2 分配问题
- 分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
示例 2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
题解:
优先满足胃口最小的孩子,把大于等于这个孩子饥饿度g[i],且最小的饼干s[j]分配给这个孩子。
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int i = 0, j = 0;
while (i < g.length && j < s.length) {
if (g[i] <= s[j]) ++i;
++j;
}
return i;
}
}
执行用时: 8 ms 内存消耗: 39.2 MB
时间复杂度:O(mlogm + nlogn),m 和 n 为数组 g 和 s 的长度,排序两个数组的时间复杂度为 mlogm + nlogn,遍历两个数组的时间复杂度为 m + n ,总时间复杂度为O(mlogm + nlogn);
空间复杂度:O(logm + logn),需要额外的空间开销 logm + logn
官方题解:
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int numOfChildren = g.length, numOfCookies = s.length;
int count = 0;
for (int i = 0, j = 0; i < numOfChildren && j < numOfCookies; i++, j++) {
while (j < numOfCookies && g[i] > s[j]) {
j++;//饼干尺寸小于孩子胃口把饼干序号加1
}
if (j < numOfCookies) {
count++;
}
}
return count;
}
}
执行用时: 9 ms 内存消耗: 39.4 MB
- 分发糖果
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
示例 1:
输入:[1,0,2]
输出:5
解释:你可以分别给这三个孩子分发 2、1、2 颗糖果。
题解:
把所有孩子的糖果数初始化为 1,先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1;再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
if (n < 2) {
//只有1个孩子情况直接返回1
return n;
}
int[] num = new int[n];//创建糖果数组初始值默认为0,返回值要加上n
for (int i = 1; i < n; ++i) {
//从左往右遍历
if (ratings[i] > ratings[i-1]) {
//如果右侧孩子评分大于左侧
num[i] = num[i-1] + 1;//右侧孩子糖果加1
}
}
for (int i = n - 1; i > 0; --i) {
//从右往左遍历
if (ratings[i] < ratings[i-1]) {
//如果右侧孩子评分小于左侧
num[i-1] = Math.max(num[i-1], num[i] + 1);//左侧糖果为原数和右侧加1的较大值
}
}
return Arrays.stream(num).sum() + n;//数组num求和并加上初始糖果数n
}
}
执行用时: 7 ms 内存消耗: 39.2 MB(在for循环中++i和i++的结果是一样的都在循环一次之后执行,但++i性能更好)
官方题解:
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int[] left = new int[n];
for (int i = 0; i < n; i++) {
if (i > 0 && ratings[i] > ratings[i - 1]) {
left[i] = left[i - 1] + 1;
} else {
left[i] = 1;
}
}
int right = 0, ret = 0;
for (int i = n - 1; i >= 0; i--) {
if (i < n - 1 && ratings[i] > ratings[i + 1]) {
right++;
} else {
right = 1;
}
ret += Math.max(left[i], right);
}
return ret;
}
}
执行用时: 3 ms 内存消耗: 39.5 MB
- 种花问题
假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。
示例 1:
输入:flowerbed = [1,0,0,0,1], n = 1
输出:true
题解:
遍历花坛中的元素,满足种花条件则n减1,返回判断n是否<=0
class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
for (int i = 0; i < flowerbed.length; ++i) {
if (flowerbed[i] == 0 && (i + 1 == flowerbed.length || flowerbed[i + 1] == 0)) {
n--;
i++;
} else if (flowerbed[i] == 1) {
i++;
}
}
return n <= 0;
}
}
执行用时: 1 ms 内存消耗: 40 MB
官方题解:
贪心策略:在不打破种植规则的情况下种入尽可能多的花,然后判断可以种入的花的最多数量是否大于或等于 n。
实现方法:
维护 prev
表示上一朵已经种植的花的下标位置,初始时prev=−1
,表示尚未遇到任何已经种植的花。
从左往右遍历数组flowerbed
,当遇到flowerbed[i]=1
时根据prev
和 i
的值计算上一个区间内可以种植花的最多数量,然后令prev=i
,继续遍历数组 flowerbed
剩下的元素。
遍历数组flowerbed
结束后,根据数组prev
和长度 m
的值计算最后一个区间内可以种植花的最多数量。
判断整个花坛内可以种入的花的最多数量是否大于或等于 n
。
class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
int count = 0;
int m = flowerbed.length;
int prev = -1;
for (int i = 0; i < m; i++) {
if (flowerbed[i] == 1) {
if (prev < 0) {
count += i / 2;
} else {
count += (i - prev - 2) / 2;
}
prev = i;
}
}
if (prev < 0) {
count += (m + 1) / 2;
} else {
count += (m - prev - 1) / 2;
}
return count >= n;
}
}
执行用时: 1 ms 内存消耗: 39.8 MB
2.3 区间问题
- 无重叠区间
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
题解:
贪心策略:优先保留结尾小且不相交的区间。
实现方法:先把区间按照结尾的大小进行增序排序,每次选择结尾最小且和前一个选择的区间不重叠的区间。
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
if (intervals.length == 0) {
return 0;
}
Arrays.sort(intervals, new Comparator<int[]>() {
//将数组集合按第二个元素大小排序
public int compare(int[] interval1, int[] interval2) {
return interval1[1] - interval2[1];
}
});
int n = intervals.length;
int total = 0, prev = intervals[0][1];//预置为数组中的第二个元素
for (int i = 1; i < n; ++i) {
if (intervals[i][0] < prev) {
//该数组的第一个元素与前一个数组的第二元素进行比较
++total;//如果prev大,区间有重叠,移除该区间
} else {
prev = intervals[i][1];//将prev置为第i个数组的第二个元素
}
}
return total;
}
}
执行用时: 3 ms 内存消耗: 38.1 MB
官方题解:
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
if (intervals.length == 0) {
return 0;
}
Arrays.sort(intervals, new Comparator<int[]>() {
public int compare(int[] interval1, int[] interval2) {
return interval1[1] - interval2[1];
}
});
int n = intervals.length;
int right = intervals[0][1];
int ans = 1;
for (int i = 1; i < n; ++i) {
if (intervals[i][0] >= right) {
++ans;
right = intervals[i][1];
}
}
return n - ans;
}
}
执行用时: 3 ms 内存消耗: 38.1 MB
- 用最少数量的箭引爆气球
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球
题解:
贪心策略:相当于435题的反面,尽量使区间重叠,注意区间可为负数
实现方法:先把区间按照结尾的大小进行增序排序 (区间可以为负数,注意此处排序方式),每次选择结尾最小且和前一个选择的区间重叠的区间。
class Solution {
public int findMinArrowShots(int[][] points) {
if (points.length == 0) {
return 0;
}
Arrays.sort(points, new Comparator<int[]> () {
public int compare (int[] point1, int[] point2) {
return point1[1] > point2[1] ? 1 : -1;
}
});
int n = points.length;
int total = n, prev = points[0][1];
for (int i = 1; i < n; ++i) {
if (points[i][0] <= prev) {
--total;
} else {
prev = points[i][1];
}
}
return total;
}
}
执行用时: 19 ms 内存消耗: 45.4 MB
官方题解:
class Solution {
public int findMinArrowShots(int[][] points) {
if (points.length == 0) {
return 0;
}
Arrays.sort(points, new Comparator<int[]>() {
public int compare(int[] point1, int[] point2) {
if (point1[1] > point2[1]) {
return 1;
} else if (point1[1] < point2[1]) {
return -1;
} else {
return 0;
}
}
});
int pos = points[0][1];
int ans = 1;
for (int[] balloon: points) {
if (balloon[0] > pos) {
pos = balloon[1];
++ans;
}
}
return ans;
}
}
执行用时: 18 ms 内存消耗: 45.7 MB
3 双指针
3.1 算法概念
双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。
3.2 两数求和
- 两数之和 II - 输入有序数组
给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2.
题解:
采用方向相反的双指针来寻找这两个数字,一个初始指向最小的元素l,即数组最左边,向右遍历;一个初始指向最大的元素r,即数组最右边,向左遍历。
class Solution {
public int[] twoSum(int[] numbers, int target) {
int l = 0, r = numbers.length - 1, sum;//新建变量l、r、sum
while (l < r) {
sum = numbers[l] + numbers[r];
if (sum == target) break;
if (sum < target) ++l;//和小于sum时把较小数增大
else --r;//反之把较大数减小
}
return new int[]{
l + 1, r + 1};//返回新建数组元素指针从0开始需要加1
}
}
执行用时: 1 ms 内存消耗: 38.6 MB
官方题解:
class Solution {
public int[] twoSum(int[] numbers, int target) {
int low = 0, high = numbers.length - 1;
while (low < high) {
int sum = numbers[low] + numbers[high];
if (sum == target) {
return new int[]{
low + 1, high + 1};
} else if (sum < target) {
++low;
} else {
--high;
}
}
return new int[]{
-1, -1};
}
}
执行用时: 1 ms 内存消耗: 38.8 MB
- 平方数之和
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c 。
示例 1:
输入:c = 5
输出:true
解释:1 * 1 + 2 * 2 = 5
题解:
使用方向相反的两个指针,假设这两个数存在,最大范围为0~sqrt©,指针l指向最左侧0,向右遍历,指针r指向sqrt©取整,向左遍历。
class Solution {
public boolean judgeSquareSum(int c) {
int l = 0, sum;
int r = (int) Math.sqrt(c);
while (l <= r) {
sum = l * l + r * r;
if (sum == c) return true;
if (sum < c) ++l;
else --r;
}
return false;
}
}
执行用时: 2 ms 内存消耗: 35.1 MB
官方题解:
class Solution {
public boolean judgeSquareSum(int c) {
long left = 0;
long right = (long) Math.sqrt(c);
while (left <= right) {
long sum = left * left + right * right;
if (sum == c) {
return true;
} else if (sum > c) {
right--;
} else {
left++;
}
}
return false;
}
}
执行用时: 4 ms 内存消耗: 35.2 MB
- 验证回文字符串 Ⅱ
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
示例 1:
输入: “aba”
输出: True
题解:
1.创建两个指针,左指针l指向0,向右遍历,右指针r指向最右,向左遍历;
2.比较两个字符是否相等,返回相应的三种情况;
3.创建新方法isPalindrome()
判断删去一个字符后的字符串是否时回文字符串。
class Solution {
public boolean validPalindrome(String s) {
int l = 0, r = s.length() - 1;
while (l < r && s.charAt(l) == s.charAt(r)) {
++l;
--r;
}
//返回三种情况,第一种s本身即为回文字符串,第二种为判断删去一个字符的情况
return l >= r || isPalindrome(s, l, r - 1) || isPalindrome(s, l + 1, r);
}
public boolean isPalindrome(String s, int l, int r) {
while (l < r && s.charAt(l) == s.charAt(r)) {
++l;
--r;
}
return l >= r;
}
}
执行用时: 7 ms 内存消耗: 38.5 MB
官方题解:
class Solution {
public boolean validPalindrome(String s) {
int low = 0, high = s.length() - 1;
while (low < high) {
char c1 = s.charAt(low), c2 = s.charAt(high);
if (c1 == c2) {
++low;
--high;
} else {
return validPalindrome(s, low, high - 1) || validPalindrome(s, low + 1, high);
}
}
return true;
}
public boolean validPalindrome(String s, int low, int high) {
for (int i = low, j = high; i < j; ++i, --j) {
char c1 = s.charAt(i), c2 = s.charAt(j);
if (c1 != c2) {
return false;
}
}
return true;
}
}
执行用时: 7 ms 内存消耗: 39 MB
3.3 归并有序数组
- 合并两个有序数组
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
题解:
把两个指针分别放在两个数组的末尾,即 nums1 的m − 1 位和 nums2 的 n − 1位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。直接利用m和n当作两个数组的指针,在创建一个pos用于确定新数组nums1的指针位置。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int pos = m-- + n-- -1;//pos起始位置为m+n-1
while (m >= 0 && n >= 0) {
nums1[pos--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--];//两数组中较大的元素加入到nums1中
}
while (n >= 0) {
//nums2还有剩余数组
nums1[pos--] = nums2[n--];
}
}
}
执行用时: 0 ms 内存消耗: 38.8 MB
官方题解:
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = 0, p2 = 0;
int[] sorted = new int[m + n];
int cur;
while (p1 < m || p2 < n) {
if (p1 == m) {
cur = nums2[p2++];
} else if (p2 == n) {
cur = nums1[p1++];
} else if (nums1[p1] < nums2[p2]) {
cur = nums1[p1++];
} else {
cur = nums2[p2++];
}
sorted[p1 + p2 - 1] = cur;
}
for (int i = 0; i != m + n; ++i) {
nums1[i] = sorted[i];
}
}
}
执行用时: 0 ms 内存消耗: 38.6 MB
- 通过删除字母匹配到字典里最长单词
给你一个字符串 s 和一个字符串数组 dictionary 作为字典,找出并返回字典中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。
如果答案不止一个,返回长度最长且字典序最小的字符串。如果答案不存在,则返回空字符串。
示例 1:
输入:s = “abpcplea”, dictionary = [“ale”,“apple”,“monkey”,“plea”]
输出:“apple”
3.4 快慢指针
- 环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
进阶:
你是否可以使用 O(1) 空间解决此题?
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
题解:
给定两个指针,分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步, slow 前进一步。如果 fast可以走到尽头,那么说明没有环路;如果 fast 可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow 和 fast 相遇。当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;//创建快慢指针
//判断是否存在环路
do {
if (fast == null || fast.next == null) return null;//不存在结点或只有一个结点返回null
fast = fast.next.next;//fast移动两个位置
slow = slow.next; //slow移动一个位置
} while (fast != slow);//不断循环当快慢指针不能相遇则不存在环路跳出循环
fast = head;//第一次相遇将fast指针移动到链表开头
//查找环路结点,快慢指针都只移动一个位置第二次相遇位置即为结点
while (fast != slow) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
执行用时: 0 ms 内存消耗: 38.7 MB
官方题解:
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head, fast = head;
while (fast != null) {
slow = slow.next;
if (fast.next != null) {
fast = fast.next.next;
} else {
return null;
}
if (fast == slow) {
ListNode ptr = head;
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
return null;
}
}
执行用时: 0 ms 内存消耗: 38.7 MB
3.5 滑动窗口
- 最小覆盖子串
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
题解:
1.遍历字符串t并记录需要的字符及其个数,用数组need映射;
2.在字符串s中建立滑动窗口,左边界l,有边界r,不断增加r使滑动窗口增大直到包含了t中的所有元素,need中所有元素<=0且count也为0;
3.不断增加l减小滑动窗口直到碰到第一个必须包含的元素,记录此时的长度;
4.再增加i,重复上述步骤直到滑动窗口再一次满足要求。
class Solution {
public String minWindow(String s, String t) {
//特殊判空情况
if (s == null || s.length() == 0 || t == null || t.length() == 0){
return "";
}
//用长度128的数组need映射字符
int[] need = new int[128];
//遍历字符串t,记录需要的字符的个数
for (int i = 0; i < t.length(); i++) {
need[t.charAt(i)]++;
}
//l是当前左边界,r是当前右边界,size记录窗口大小,count是需求的字符个数,start是最小覆盖串开始的index
int l = 0, r = 0, size = Integer.MAX_VALUE, count = t.length(), start = 0;
//遍历s的所有字符
while (r < s.length()) {
if (need[s.charAt(r)] > 0) {
count--;
}
need[s.charAt(r)]--;//把右边的字符加入窗口,s中的字符减少1
if (count == 0) {
//窗口中已经包含所有字符
while (l < r && need[s.charAt(l)] < 0) {
need[s.charAt(l)]++;//释放右边移动出窗口的字符
l++;//指针右移
}
if (r - l + 1 < size) {
//不能右移时候挑战最小窗口大小,更新最小窗口开始的start
size = r - l + 1;
start = l;//记录下最小值时候的开始位置,最后返回覆盖串时候会用到
}
//l向右移动后窗口肯定不能满足了 重新开始循环
need[s.charAt(l)]++;
l++;
count++;
}
r++;
}
return size == Integer.MAX_VALUE ? "" : s.substring(start, start + size);
}
}
执行用时: 5 ms 内存消耗: 38.4 MB
官方题解:
class Solution {
Map<Character, Integer> ori = new HashMap<Character, Integer>();
Map<Character, Integer> cnt = new HashMap<Character, Integer>();
public String minWindow(String s, String t) {
int tLen = t.length();
for (int i = 0; i < tLen; i++) {
char c = t.charAt(i);
ori.put(c, ori.getOrDefault(c, 0) + 1);
}
int l = 0, r = -1;
int len = Integer.MAX_VALUE, ansL = -1, ansR = -1;
int sLen = s.length();
while (r < sLen) {
++r;
if (r < sLen && ori.containsKey(s.charAt(r))) {
cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1);
}
while (check() && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l;
ansR = l + len;
}
if (ori.containsKey(s.charAt(l))) {
cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
}
++l;
}
}
return ansL == -1 ? "" : s.substring(ansL, ansR);
}
public boolean check() {
Iterator iter = ori.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Character key = (Character) entry.getKey();
Integer val = (Integer) entry.getValue();
if (cnt.getOrDefault(key, 0) < val) {
return false;
}
}
return true;
}
}
执行用时: 118 ms 内存消耗: 39 MB
时间复杂度:最坏情况下左右指针对 s 的每个元素各遍历一遍,哈希表中对 s 中的每个元素各插入、删除一次,对 t 中的元素各插入一次。每次检查是否可行会遍历整个 t 的哈希表,哈希表的大小与字符集的大小有关,设字符集大小为 C,则渐进时间复杂度为 O(C⋅∣s∣+∣t∣)。
空间复杂度:这里用了两张哈希表作为辅助空间,每张哈希表最多不会存放超过字符集大小的键值对,我们设字符集大小为 C ,则渐进空间复杂度为 O( C )。
4 二分查找
4.1 算法概念
二分查找(Binary Search)也叫作折半查找。二分查找有两个要求,一个是数列有序,另一个是数列使用顺序存储结构(比如数组)。长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)。
4.2 开平方
- x 的平方根
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
题解:
将开方转化为给定一个非负整数x,求 f ® = r^2 − x = 0 的解。
单独考虑x == 0的情况,然后再[1,x]区间内用二分查找法。
class Solution {
public int mySqrt(int x) {
if (x == 0) return x;
int l = 1, r = x, mid, sqrt;
while (l <= r) {
mid = l + (r - l) /2;
sqrt = x / mid;
if (sqrt == mid) {
return mid;
} else if (mid > sqrt) {
r = mid - 1;
} else {
l = mid + 1;
}
}
return r;
}
}
执行用时: 1 ms 内存消耗: 35.4 MB
官方题解:
class Solution {
public int mySqrt(int x) {
int l = 0, r = x, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if ((long) mid * mid <= x) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return ans;
}
}
执行用时: 1 ms 内存消耗: 35.6 MB
4.3 查找区间
- 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
进阶:
你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
题解:
用二分法分别查找数组中的target。
class Solution {
public int[] searchRange(int[] nums, int target) {
if (nums == null || nums.length == 0) return new int[]{
-1, -1};
int lower = lower_bound(nums, target);
int upper = upper_bound(nums, target) - 1;
if (lower == nums.length || nums[lower] != target) {
return new int[]{
-1, -1};
}
return new int[]{
lower, upper};
}
//二分查找序号低的target
int lower_bound(int[] nums, int target) {
int l = 0, r = nums.length, mid;
while (l < r) {
mid = (l + r) / 2;
if (nums[mid] >= target) {
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
//二分查找序号高的target
int upper_bound(int[] nums, int target) {
int l = 0, r = nums.length, mid;
while (l < r) {
mid = (l + r) / 2;
if (nums[mid] > target) {
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
}
执行用时: 0 ms 内存消耗: 41.7 MB
官方题解:
class Solution {
public int[] searchRange(int[] nums, int target) {
int leftIdx = binarySearch(nums, target, true);
int rightIdx = binarySearch(nums, target, false) - 1;
if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] == target && nums[rightIdx] == target) {
return new int[]{
leftIdx, rightIdx};
}
return new int[]{
-1, -1};
}
public int binarySearch(int[] nums, int target, boolean lower) {
int left = 0, right = nums.length - 1, ans = nums.length;
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}
}
执行用时: 0 ms 内存消耗: 41.4 MB
- 有序数组中的单一元素
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
示例 1:
输入: [1,1,2,3,3,4,4,8,8]
输出: 2
注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。
题解:
class Solution {
public int singleNonDuplicate(int[] nums) {
int l = 0, r = nums.length - 1;
while (l < r) {
int mid = l + (r - l) / 2;
//mid为偶数且与后一个元素相等,只出现一次元素在mid右侧
if (mid % 2 == 0 && nums[mid] == nums[mid + 1]) {
l = mid + 2;
//mid为奇数且与前一个元素相等,只出现一次元素在mid右侧
} else if (mid % 2 == 1 && nums[mid - 1] == nums[mid]) {
l = mid + 1;
//只出现一次元素在左侧
} else {
r = mid;
}
}
return nums[l];
}
}
执行用时: 0 ms 内存消耗: 38.7 MB
官方题解:
class Solution {
public int singleNonDuplicate(int[] nums) {
int lo = 0;
int hi = nums.length - 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (mid % 2 == 1) mid--;
if (nums[mid] == nums[mid + 1]) {
lo = mid + 2;
} else {
hi = mid;
}
}
return nums[lo];
}
}
执行用时: 0 ms 内存消耗: 38.5 MB
4.4 旋转数组
- 搜索旋转排序数组 II
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
示例 1:
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
进阶:
这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。
这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?
题解:
数组旋转后,仍旧保留部分有序和递增,利用这个特性进行二分查找。
对于当前mid,如果该值<=右侧,那么右侧有序,反之左侧有序。
如果target位于有序区间内,继续对这个区间二分查找,反之对另一半区间二分查找。
因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找。
class Solution {
public boolean search(int[] nums, int target) {
int start = 0, end = nums.length - 1;
while (start <= end) {
int mid = (start + end) / 2;
//恰好找到target直接返回true
if (nums[mid] == target) {
return true;
}
if (nums[start] == nums[mid]) {
//无法判断增序
++start;
} else if (nums[mid] <= nums[end]) {
//右区间增序
if (target > nums[mid] && target <= nums[end]) {
start = mid + 1;
} else {
end = mid - 1;
}
} else {
//左区间增序
if (target >= nums[start] && target < nums[mid]) {
end = mid - 1;
} else {
start = mid + 1;
}
}
}
return false;
}
}
执行用时: 1 ms 内存消耗: 38.1 MB
官方题解:
class Solution {
public boolean search(int[] nums, int target) {
int n = nums.length;
if (n == 0) {
return false;
}
if (n == 1) {
return nums[0] == target;
}
int l = 0, r = n - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == target) {
return true;
}
if (nums[l] == nums[mid] && nums[mid] == nums[r]) {
++l;
--r;
} else if (nums[l] <= nums[mid]) {
if (nums[l] <= target && target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[n - 1]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return false;
}
}
执行用时: 1 ms 内存消耗: 37.6 MB
- 寻找旋转排序数组中的最小值 II
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。
给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
示例 1:
输入:nums = [1,3,5]
输出:1
题解:
当 nums[mid] > nums[right]时,最小值一定在mid右侧,此时i一定满足 mid < i <= right,因此执行 left = mid + 1;
当 nums[mid] < nums[right] 时,最小值一定在mid左侧,此时i一定满足 left < i <= mid,因此执行 right = mid;
当 nums[mid] == nums[right] 时,有重复元素无法判断,因此将最右减-1.
class Solution {
public int findMin(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
int mid = (left + right) / 2;
//最小值在右侧
if (nums[mid] > nums[right]) left = mid + 1;
//最小值在左侧
else if (nums[mid] < nums[right]) right = mid;
//重复数字无法判断
else right = right - 1;
}
return nums[left];
}
}
执行用时: 0 ms 内存消耗: 38.1 MB
官方题解:
class Solution {
public int findMin(int[] nums) {
int low = 0;
int high = nums.length - 1;
while (low < high) {
int pivot = low + (high - low) / 2;
if (nums[pivot] < nums[high]) {
high = pivot;
} else if (nums[pivot] > nums[high]) {
low = pivot + 1;
} else {
high -= 1;
}
}
return nums[low];
}
}
执行用时: 1 ms 内存消耗: 38.2 MB
5 排序算法
5.1 排序算法大全
算法分类:
算法复杂度:
算法详解:
十大排序算法
1 冒泡排序:
①比较相邻的元素。如果第一个比第二个大,就交换他们两个。
②对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
③针对所有的元素重复以上的步骤,除了最后一个,持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
优化: 增加 flag 变量,遍历一遍后没有发生交换,则数组已经有序,直接跳出循环返回数组。
public static int[] BubbleSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
boolean flag = true;
for (int j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = false;
}
}
if (flag) break;
}
return arr;
}
2 选择排序
①首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
②再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
③重复第二步,直到所有元素均排序完毕。
图中红色为 minIndex ,绿色为 j
public static int[] SelectionSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
int tmp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = tmp;
}
}
return arr;
}
3 插入排序
①将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
②从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
public static int[] InsertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int preIndex = i - 1;
int current = arr[i];
while (preIndex >= 0 && current < arr[preIndex]) {
arr[preIndex + 1] = arr[preIndex];
preIndex -= 1;
}
arr[preIndex + 1] = current;
}
return arr;
}
4 希尔排序
希尔排序是一种插入排序,简单插入排序后改进的高效版本,也称为递减增量排算法。
①选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
②按增量序列个数 k,对序列进行 k 趟排序;
③每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
public static int[] ShellSort(int[] arr) {
int n = arr.length;
int gap = n / 2;
while (gap > 0) {
for (i = gap; i < n; i++) {
int preIndex = i - gap;
int current = arr[i];
while (preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + gap] = arr[preIndex];
preIndex -= gap;
}
arr[preIndex + gap] = current;
}
gap /= 2;
}
return arr;
}
5 归并排序
①申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
②设定两个指针,最初位置分别为两个已经排序序列的起始位置;
③比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
⑥重复步骤 3 直到某一指针达到序列尾;
⑦将另一序列剩下的所有元素直接复制到合并序列尾。
public static int[] MergeSort(int[] arr) {
if (arr.length <= 1) return arr;
int middle = length / 2;
int[] arr_1 = Arrays.copyOfRange(arr, 0, middle);
int[] arr_2 = Arrays.copyOfRange(arr, middle, arr.length);
return Merge(MergeSort(arr_1), MergeSort(arr_2));
}
public static int[] Merge(int[] arr_1, int[] arr_2) {
int[] sorted_arr = new int[arr_1.length + arr_2.length];
int idx = 0, idx_1 = 0, idx_2 = 0;
while(idx_1 < arr_1.length && idx_2 < arr_2.length) {
if (arr_1[idx_1] < arr_2[idx_2]) {
sorted_arr[idx] = arr_1[idx_1];
idx_1 += 1;
} else {
sorted_arr[idx] = arr_2[idx_2];
idx_2 += 1;
}
idx += 1;
}
if (idx_1 < arr_1.length) {
while(idx_1 < arr_1.length) {
sorted_arr[idx] = arr_1[idx_1];
idx_1 += 1;
idx += 1;
}
} else {
while(idx_2 < arr_2.length) {
sorted_arr[idx] = arr_2[idx_2];
idx_2 += 1;
idx+= 1;
}
}
return aorted_arr;
}
6 快速排序
冒泡排序基础上的递归分治法。快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。平均期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
①从数列中挑出一个元素,称为 “基准”(pivot);
②重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
③递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
颜色 | 参数 |
---|---|
黄色 | pivot |
红色 | a[i] |
紫色 | a[i] > pivot |
橙色 | 已排序 |
绿色 | a[i] < pivot |
双指针交换法:
public static int[] QuickSort(int[] arr, int left, int right) {
if (left < right) {
int pivotIndex = Partition(arr, left, right);
Quicksort(arr, left, pivotIndex - 1);
Quicksort(arr, pivotIndex + 1, right);
}
return arr;
}
public static int Partiton(int[] arr, int left, int right) {
int pivot = arr[left];
int start = left;
while(left < right) {
while(left < right && arr[right] >= pivot) right--;
while(left < right && arr[left] <= pivot) left++;
if (left >= right) break;
swap(arr, left, right);
}
swap(arr, start, left);
return left;
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
挖坑填数法:
public static int[] QuickSort(int[] arr, int left, int right) {
if (left < right) {
int pivotIndex = Partition(arr, left, right);
Quicksort(arr, left, pivotIndex - 1);
Quicksort(arr, pivotIndex + 1, right);
}
return arr;
}
public static int Partition(int[] arr, int left, int right) {
int pivot = arr[left];
while(left < right) {
while(left < right && arr[right] >= pivot) right--;
if (left < right) arr[left] = arr[right];
while(left < right && arr[left] <= pivot) left++;
if (left < right) arr[right] = arr[left];
}
arr[left] = pivot;
return left;
}
归并排序和快速排序区别:
①归并排序是自下而上的,先处理子问题,然后再合并,将小集合合成大集合,最后实现排序;
②快速排序是由上到下的,先分区,然后再处理子问题。
使用栈实现迭代:
利用栈先进后出的特性实现迭代
public static int[] QuickSort(int[] arr, int left, int right) {
Stack<Integer> stack = new Stack<>();
stack.push(arr.length - 1);
stack.push(0);
while(!stack.isEmpty()) {
int left = stack.pop();
int right = stack.pop();
if (left < right) {
int pivotIndex = Partition(arr, left, right);
stack.push(pivotIndex - 1);
stack.push(left);
stack.push(right);
stack.push(pivotIndex + 1);
}
}
return arr;
}
public static int Partiton(int[] arr, int left, int right) {
int pivot = arr[left];
int start = left;
while(left < right) {
while(left < right && arr[right] >= pivot) right--;
while(left < right && arr[left] <= pivot) left++;
if (left >= right) break;
swap(arr, left, right);
}
swap(arr, start, left);
return left;
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
基准值选取优化:
三数取中法:
选取基准值时 left 可能恰好为最大值或最小值,迭代耗时,选取时应尽量避免选取序列的最大或最小值做为基准值。
int mid = left + ((right - left) >> 1);
if (arr[left] > arr[right]) swap(arr, left, right);
if (arr[mid] > arr[right]) swap(arr, mid, right);
if (arr[mid] > arr[left]) swap(arr, mid, left);
7 堆排序
利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
①创建一个堆 H[0……n-1];
②把堆首(最大值)和堆尾互换;
③把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
④重复步骤 2,直到堆的尺寸为 1。
public static int[] HeapSort(int[] arr) {
// index at the end of the heap
heapLen = arr.length;
// build MaxHeap
buildMaxHeap(arr);
for (int i = arr.length - 1; i > 0; i--) {
// Move the top of the heap to the tail of the heap in turn
swap(arr, 0, i);
heapLen -= 1;
heapify(arr, 0);
}
return arr;
}
private static void heapify(int[] arr, int i) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (right < heapLen && arr[right] > arr[largest]) {
largest = right;
}
if (left < heapLen && arr[left] > arr[largest]) {
largest = left;
}
if (largest != i) {
swap(arr, largest, i);
heapify(arr, largest);
}
}
private static void buildMaxHeap(int[] arr) {
for (int i = arr.length / 2 - 1; i >= 0; i--) {
heapify(arr, i);
}
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
8 计数排序
①找出数组中的最大值maxValue、最小值minValue;
②创建一个新数组countArr,其长度是maxValue - minValue + 1,其元素默认值都为0;
③遍历原数组 arr 中的元素 arr[i],以 arr[i]-minValue作为 countArr 数组的索引,以 arr[i] 的值在 arr 中元素出现次数作为countArr[arr[i] - minValue] 的值;
④对 countArr 数组变形,新元素的值是该元素与前一个元素值的和,即当 i > 1 时 countArr[i] = countArr[i] + countArr[i-1];
⑤创建结果数组 result,长度和原始数组一样。
⑥从后向前遍历原始数组 arr 中的元素 arr[i],使用 arr[i] 减去最小值 minValue 作为索引,在计数数组 countArr 中找到对应的值 countArr[arr[i] - minValue],countArr[arr[i] - minValue] - 1就是 arr[i] 在结果数组 result 中的位置,做完上述这些操作,将countArr[arr[i]-minValue]减小1。
public static int[] CountingSort(int[] arr) {
if (arr.length < 2) {
return arr;
}
int[] extremum = getMinAndMax(arr);
int minValue = extremum[0];
int maxValue = extremum[1];
int[] countArr = new int[maxValue - minValue + 1];
int[] result = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
countArr[arr[i] - minValue] += 1;
}
for (int i = 1; i < countArr.length; i++) {
countArr[i] += countArr[i - 1];
}
for (int i = arr.length - 1; i >= 0; i--) {
int idx = countArr[arr[i] - minValue] - 1;
result[idx] = arr[i];
countArr[arr[i] - minValue] -= 1;
}
return result;
}
private static int[] getMinAndMax(int[] arr) {
int maxValue = arr[0];
int minValue = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > maxValue) {
maxValue = arr[i];
} else if (arr[i] < minValue) {
minValue = arr[i];
}
}
return new int[] {
minValue, maxValue };
}
9 桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
①设置一个BucketSize,作为每个桶所能放置多少个不同数值;
②遍历输入数据,并且把数据依次映射到对应的桶里去;
③对每个非空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;
④从非空桶里把排好序的数据拼接起来。
public static List<Integer> BucketSort(List<Integer> arr, int bucket_size) {
if (arr.size() < 2 || bucket_size == 0) {
return arr;
}
int[] extremum = getMinAndMax(arr);
int minValue = extremum[0];
int maxValue = extremum[1];
int bucket_cnt = (maxValue - minValue) / bucket_size + 1;
List<List<Integer>> buckets = new ArrayList<>();
for (int i = 0; i < bucket_cnt; i++) {
buckets.add(new ArrayList<Integer>());
}
for (int element : arr) {
int idx = (element - minValue) / bucket_size;
buckets.get(idx).add(element);
}
for (int i = 0; i < buckets.size(); i++) {
if (buckets.get(i).size() > 1) {
buckets.set(i, sort(buckets.get(i), bucket_size / 2));
}
}
ArrayList<Integer> result = new ArrayList<>();
for (List<Integer> bucket : buckets) {
for (int element : bucket) {
result.add(element);
}
}
return result;
}
private static int[] getMinAndMax(List<Integer> arr) {
int maxValue = arr.get(0