*数组中重复的数字
1 题目要求
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。
请找出数组中任意一个重复的数字。
2 解题(Java)
2.1 解题思路
- 注意到数组中的数字都在 0 ~ n-1 的范围内。如果将数字i放置在下标为i的位置,由于数组中有重复的数字,有些位置可能存在多个数字,同时可能有些位置没有数字;
- 从头到尾依次扫描这个数组中的每个数字。当扫描到下标为i的数字时,首先比较这个数字(用m表示)是不是等于i。如果是,则接着扫描下一个数字;如果不是,则再拿它和第m个数字进行比较。如果它和第m个数字相等,就找到了一个重复的数字(该数字在下标为i和m的位置都出现了);如果它和第m个数字不相等,就把第i个数字和第m个数字交换,把m放到属于它的位置。接下来再重复这个比较、交换的过程,直到我们发现一个重复的数字;
2.2 代码
class Solution {
public int findRepeatNumber(int[] nums) {
int i = 0;
while (i < nums.length) {
if (nums[i] == i) {
i++;
continue;
} else if (nums[nums[i]] == nums[i]) {
return nums[i];
} else {
int tmp = nums[i];
nums[i] = nums[tmp];
nums[tmp] = tmp;
}
}
return -1;
}
}
3 复杂度分析
- 时间复杂度O(N):遍历数组使用 O(N) ,每轮遍历的判断和交换操作使用 O(1);
- 空间复杂度O(1);
*根据身高重建队列
1 题目描述
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
示例 1:
输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5,没有身高更高或者相同的人排在他前面。 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 因此[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
示例 2:
输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
提示:
- 1 <= people.length <= 2000
- 0 <= hi <= 106
- 0 <= ki < people.length
- 题目数据确保队列可以被重建
2 解题(Java)
排序
/**
* 解题思路:先排序再插入
* 1.排序规则:按照先H高度降序,K个数升序排序
* 2.遍历排序后的数组,根据K插入到K的位置上
* * 核心思想:高个子先站好位,矮个子插入到K位置上,前面肯定有K个高个子,矮个子再插到前面也满足K的要求
*/
class Solution {
public int[][] reconstructQueue(int[][] people) {
// [7,0], [7,1], [6,1], [5,0], [5,2], [4,4]
// 再一个一个插入。
// [7,0]
// [7,0], [7,1]
// [7,0], [6,1], [7,1]
// [5,0], [7,0], [6,1], [7,1]
// [5,0], [7,0], [5,2], [6,1], [7,1]
// [5,0], [7,0], [5,2], [6,1], [4,4], [7,1]
Arrays.sort(people, (o1, o2) -> o1[0] == o2[0] ? o1[1] - o2[1] : o2[0] - o1[0]);
List<int[]> res = new LinkedList<>();
for (int[] p : people) {
res.add(p[1], p);
}
return res.toArray(new int[][]{});
}
}
3 复杂性分析
- 时间复杂度:O(nlogn),其中 n 是数组people 的长度。需要O(nlogn) 的时间进行排序,随后需要 O(n)时间遍历每一个人插入列表里,因此总时间复杂度为 O(nlogn);
- 空间复杂度:O(logn),即为排序需要使用的栈空间;
*移动零
1 题目描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
- 必须在原数组上操作,不能拷贝额外的数组。
- 尽量减少操作次数。
2 解题(Java)
快排思想:
class Solution {
public void moveZeroes(int[] nums) {
if (nums == null) return;
int left = 0;
for (int right=0; right<nums.length; right++) {
if (nums[right] != 0) {
int temp = nums[right];
nums[right] = nums[left];
nums[left++] = temp;
}
}
}
}
3 复杂性分析
- 时间复杂度: O(n)
- 空间复杂度: O(1)
*数组中的第K个最大元素
1 题目描述
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
说明:
你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。
2 解题(Java)
解题思路
-
可以用快速排序来解决这个问题,先对原数组排序,再返回倒数第 k 个位置,这样平均时间复杂度是 O(nlogn),但其实可以做得更快;
-
对数组 a[l⋯r] 做快速排序的过程如下:
- 分解: 将数组 a[l⋯r] 「划分」成两个子数组 a[l⋯q−1]、a[q+1⋯r],使得 a[l⋯q−1] 中的每个元素小于等于 a[q],且 a[q] 小于等于 a[q+1⋯r] 中的每个元素。其中,计算下标 q 也是「划分」过程的一部分;
- 解决: 通过递归调用快速排序,对子数组 a[l⋯q−1] 和 a[q+1⋯r] 进行排序;
- 合并: 因为子数组都是原址排序的,所以不需要进行合并操作,a[l⋯r] 已经有序;
- 上文中提到的 「划分」 过程是:从子数组 a[l⋯r] 中选择任意一个元素 x 作为主元,调整子数组的元素使得左边的元素都小于等于它,右边的元素都大于等于它, x 的最终位置就是 q;
-
由此可以发现每次经过「划分」操作后,我们一定可以确定一个元素的最终位置,即 x 的最终位置为 q,并且保证 a[l⋯q−1] 中的每个元素小于等于 a[q],且 a[q] 小于等于 a[q+1⋯r] 中的每个元素。所以只要某次划分的 q 为倒数第 k 个下标的时候,我们就已经找到了答案;
-
因此我们可以改进快速排序算法来解决这个问题:在分解的过程当中,我们会对子数组进行划分,如果划分得到的 q 正好就是我们需要的下标,就直接返回 a[q];否则,如果 q 比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。这就是「快速选择」算法;
-
我们知道快速排序的性能和「划分」出的子数组的长度密切相关。直观地理解如果每次规模为 n 的问题我们都划分成 1 和 n−1,每次递归的时候又向 n−1 的集合中递归,这种情况是最坏的,时间代价是 O(n 2)。我们可以引入随机化来加速这个过程,它的时间代价的期望是 O(n),证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」;
代码
class Solution {
Random random = new Random();
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, 0, nums.length - 1, nums.length - k);
}
public int quickSelect(int[] a, int l, int r, int index) {
int q = randomPartition(a, l, r);
if (q == index) {
return a[q];
} else {
return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
}
}
public int randomPartition(int[] a, int l, int r) {
int i = random.nextInt(r - l + 1) + l;
swap(a, i, l);
return partition(a, l, r);
}
public int partition(int[] a, int l, int r) {
int x = a[l], left = l, right = r;
while (left < right) {
while (left < right && a[right] >= x) {
right--;
}
while (left < right && a[left] <= x) {
left++;
}
swap(a, left, right);
}
swap(a, l, left);
return left;
}
public void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
3 复杂性分析
- 时间复杂度O(n):如上文所述,证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」;
- 空间复杂度O(logn):递归使用栈空间的空间代价的期望为 O(logn);
*下一个排列
1 题目描述
实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须 原地 修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
示例 4:
输入:nums = [1]
输出:[1]
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 100
2 解题(Java)
算法流程
- 首先从后向前查找第一个顺序对 (i,i+1),满足 a[i] < a[i+1]。这样「较小数」即为 a[i]。此时 [i+1,n) 必然是下降序列;
- 如果找到了顺序对,那么在区间 [i+1,n)中从后向前查找第一个元素 j满足 a[i] < a[j]。这样「较大数」即为 a[j];
- 交换 a[i] 与 a[j],此时可以证明区间 [i+1,n) 必为降序。我们可以直接使用双指针反转区间 [i+1,n)使其变为升序,而无需对该区间进行排序;
- 如果在步骤 1 找不到顺序对,说明当前序列已经是一个降序序列,即最大的序列,我们直接跳过步骤 2 执行步骤 3,即可得到最小的升序序列;
代码
class Solution {
public void nextPermutation(int[] nums) {
if (nums.length <= 1) return;
int i = nums.length - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i >= 0) {
int j = nums.length - 1;
while (nums[i] >= nums[j]) {
j--;
}
swap(nums, i, j);
}
reverse(nums, i + 1);
}
public void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
public void reverse(int[] nums, int start) {
int left = start, right = nums.length - 1;
while (left < right) {
swap(nums, left, right);
left++;
right--;
}
}
}
3 复杂性分析
- 时间复杂度O(N):其中 N 为给定序列的长度,我们至多只需要扫描两次序列,以及进行一次反转操作;
- 空间复杂度O(1):只需要常数的空间存放若干变量;
*合并区间
1 题目描述
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
提示:
- 1 <= intervals.length <= 10 ^ 4
- intervals[i].length == 2
- 0 <= starti <= endi <= 10 ^ 4
2 解题(Java)
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length == 0) {
return new int[0][0];
}
Arrays.sort(intervals, (o1, o2) -> o1[0] - o2[0]);
List<int[]> merged = new ArrayList<>();
for (int i=0; i<intervals.length; i++) {
int L = intervals[i][0], R = intervals[i][1];
// 如果列表为空,或者当前区间与上一区间不重合,直接添加
if (merged.size() == 0 || merged.get(merged.size() -1)[1] < L) {
merged.add(new int[]{L, R});
}
// 否则与上一区间合并
else {
merged.get(merged.size()-1)[1] = Math.max(merged.get(merged.size()-1)[1], R);
}
}
return merged.toArray(new int[][]{});
}
}
3 复杂性分析
- 时间复杂度O(nlogn):其中 n 为区间的数量。除去排序的开销,只需加一次线性扫描,所以主要的时间开销是排序的 O(nlogn);
- 空间复杂度O(logn):除了存储答案之外,使用的额外空间即为排序所需要的空间复杂度O(logn);
*二维数组中的查找
1 题目描述
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例:
现有二维数组如下:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。
给定 target = 20,返回 false。
2 解题(Java)
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if (matrix.length==0 || matrix[0].length==0) return false;
int rows = matrix.length, columns = matrix[0].length;
int row = 0, column = columns-1;
while(row <= rows-1 && column >= 0) {
if (matrix[row][column] > target) column--;
else if (matrix[row][column] < target) row++;
else return true;
}
return false;
}
}
3 复杂性分析
- 时间复杂度:O(n+m)。访问到的下标的行最多增加 n 次,列最多减少 m 次,因此循环体最多执行 n + m 次;
- 空间复杂度:O(1);
*数组中的逆序对
1 题目描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
输入: [7,5,6,4]
输出: 5
限制:
0 <= 数组长度 <= 50000
2 解题(Java)
归并排序法,并的过程中统计个数:
class Solution {
int count;
int[] assist;
public int reversePairs(int[] nums) {
int len = nums.length;
assist = new int[len];
sort(nums, 0, len - 1);
return count;
}
public void sort(int[] nums, int left, int right) {
if (left >= right) return;
int mid = (left + right) / 2;
sort(nums, left, mid);
sort(nums, mid + 1, right);
mergeAndCount(nums, left, mid, right);
}
public void mergeAndCount(int[] nums, int left, int mid, int right) {
int p1 = left, p2 = mid + 1;
int index = left;
while (p1 <= mid && p2 <= right) {
if (nums[p1] > nums[p2]) {
assist[index++] = nums[p2++];
count += mid - p1 + 1;
} else {
assist[index++] = nums[p1++];
}
}
while (p1 <= mid) assist[index++] = nums[p1++];
while (p2 <= right) assist[index++] = nums[p2++];
for (index = left; index <= right; index++) {
nums[index] = assist[index];
}
}
}
3 复杂性分析
- 时间复杂度O(NlogN):同归并排序O(NlogN);
- 空间复杂度O(N):同归并排序 O(N),因为归并排序需要用到一个临时数组;
把数组排成最小的数
1 题目描述
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
示例 1:
输入: [10,2]
输出: “102”
示例 2:
输入: [3,30,34,5,9]
输出: “3033459”
提示:
0 < nums.length <= 100
说明:
- 输出结果可能非常大,所以你需要返回一个字符串而不是整数;
- 拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0;
2 解题(Java)
2.1 解题思路
- 此题求拼接起来的 “最小数字” ,本质上是一个排序问题;
- 排序判断规则: 设 nums 任意两数字的字符串格式 x 和 y ,则
- 若拼接字符串 x + y > y + x,则 x > y;
- 反之,若 x + y < y + x,则 x < y;
- 根据以上规则,套用任何排序方法对 nums 执行排序即可;
2.2 算法流程
- 初始化: 字符串列表 strs ,保存各数字的字符串格式;
- 列表排序: 应用以上 “排序判断规则” ,对 strs 执行排序;
- 返回值: 拼接 strs 中的所有字符串,并返回;
2.3 代码(自定义快速排序)
class Solution {
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]);
}
fastSort(strs, 0, strs.length - 1);
StringBuilder res = new StringBuilder();
for (String s : strs) {
res.append(s);
}
return res.toString();
}
void fastSort(String[] strs, int lo, int hi) {
if (lo >= hi) return;
int left = lo, right = hi;
while (left < right) {
while (left < right && (strs[right] + strs[lo]).compareTo(strs[lo] + strs[right]) >= 0) {
right--;
}
while (left < right && (strs[left] + strs[lo]).compareTo(strs[lo] + strs[left]) <= 0) {
left++;
}
String tmp = strs[left];
strs[left] = strs[right];
strs[right] = tmp;
}
String tmp = strs[left];
strs[left] = strs[lo];
strs[lo] = tmp;
fastSort(strs, lo, left - 1);
fastSort(strs, left + 1, hi);
}
}
2.4 代码(内置函数排序)
class Solution {
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();
}
}
3 复杂性分析
- 时间复杂度 O(NlogN) : 使用快排或内置函数的平均时间复杂度为 O(NlogN) ,最差为 O(N^2);
- 空间复杂度 O(N) : 字符串列表 strs 占用线性大小的额外空间;