189、难度中等:内容参考了官方答案
题意:把数组想成环首尾相连,右移相当于转动环
要求:1、使用空间复杂度为 O(1) 的 原地 算法解决这个问题;2、至少有三种不同的方法可以解决这个问题
解题思路:
1、O(1)就是最低的时空复杂度,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。
如int一个变量,无论数值多大耗时耗空间都不变。而循环则不痛,数值越大循环次数越多,耗时越多。
2、由于要求原地解决所以我们应创造出一个公式来解决位置变换问题。
旋转取得当前元素下一个位置的公式应该满足在元素处于n位置时可以让其到达首位置。也就是取余数。
方法一:使用额外数组:时间复杂度:O(n),其中n为数组的长度。空间复杂度: O(n)。
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
int[] newArr = new int[n];
for (int i = 0; i < n; ++i) {
newArr[(i + k) % n] = nums[i];
}
System.arraycopy(newArr, 0, nums, 0, n);
}
}
1、System.arraycopy(newArr, 0, nums, 0, n);表达为:将newArr源数组中从0位到第n位之间的数值copy到nums目标数组中,在目标数组的第0位开始放置。
2、取余数:(i + k) % n:假设当前数组长度为n,当前元素为第i项,要右移k项。若i+k无法到达n位置,那即便(i+k)%n也等于i+k本身。
若超过了n,那我们可以想成把k的一部分用来与i相加后等于n,然后实际旋转后的位置就是k除那部分以外的剩余部分。
这剩余的部分怎么求:可以吧i+k看成a个n(a为正整数)和剩余的小于n的最终实际位置的部分。所以直接取余即可。
方法二:环状替换:时间复杂度:O(n),其中 n为数组,的长度。每个元素只会被遍历一次,空间复杂度:O(1)。我们只需常数空间存放若干变量。
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k = k % n; //若k小于n则还是k本身,若大于直接取余,除去n的整数倍(不是必须i+k取余,k本身也属于位置信息可以单独取余)
int count = gcd(k, n); //该方法原理看下面的推导过程,count是我们所需的总遍历数
for (int start = 0; start < count; ++start) { //从下标0开始遍历,0完了1再完了2,一共遍历到count-1为止
int current = start;
int prev = nums[start]; //前一项(准备向右移动的项)
do {
int next = (current + k) % n; //当前项要移动到的位置
int temp = nums[next]; //把移动位置的项数值保存下来不然丢失数据
nums[next] = prev; //往新位置里放入当前项
prev = temp; //现在的当前项就是失去原本位置,即将被放入其移动后位置的项
current = next; //current(现在):要处理元素的原位置(next已经被别的元素占据了)
} while (start != current); //若当前待处理位置和本轮遍历的起始位置相同那就说明可以进行下一轮遍历(原理在下面)
}
}
public int gcd(int x, int y) {
return y > 0 ? gcd(y, x % y) : x;
}
}
方法一中使用额外数组的原因在于如果我们直接将每个数字放至它最后的位置,这样被放置位置的元素会被覆盖从而丢失。因此,从另一个角度,我们可以将被替换的元素保存在变量temp 中,从而避免了额外数组的开销。
我们从位置 0 开始,最初令temp=nums[0]。根据规则,位置 0 的元素会放至 (0+k)mod n 的位置,令 x=(0+k)\bmod nx=(0+k)mod n,此时交换temp 和 nums[x],完成位置 x 的更新(temp保留当前被替换位置元素的值 / 下一个待处理元素的值)然后,我们考察位置 x,并交换temp 和nums[(x+k)mod n],从而完成下一个位置的更新。不断进行上述过程,直至回到初始位置 0(一轮遍历结束,该轮遍历处理了不止1个元素)
容易发现,当回到初始位置 0 时,有些数字可能还没有遍历到,此时我们应该从下一个数字开始重复的过程,可是这个时候怎么才算遍历结束呢?我们不妨先考虑这样一个问题:从 0 开始不断遍历,最终回到起点 0 的过程中,我们遍历了多少个元素?
由于最终回到了起点,故该过程恰好走了整数数量的圈,不妨设为 a 圈;再设该过程总共遍历了 b 个元素。因此,我们有 a * n=b * k,即 an 一定为n,k 的公倍数。又因为我们在第一次回到起点时就结束,因此 a 要尽可能小,故 an 就是n,k 的最小公倍数lcm(n,k),因此 b 就为 lcm(n,k)/k。
这说明单次遍历会访问到 klcm(n,k)/k 个元素。为了访问到所有的元素,我们需要进行遍历的次数为
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ni0cYdQ1-1631785496384)(189.旋转数组.assets/image-20210916160247657.png)]
其中 gcd 指的是最大公约数。一个具体例子如下图(nums = [1, 2, 3, 4, 5, 6],k = 2)
方法三:数组翻转:时间复杂度:O*(*n),其中 n为数组的长度。每个元素被翻转两次,一共 n 个元素,因此总时间复杂度为 O(2n)=O(n),空间复杂度:O(1)
class Solution {
public void rotate(int[] nums, int k) { //主方法
k %= nums.length;
reverse(nums, 0, nums.length - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, nums.length - 1);
}
public void reverse(int[] nums, int start, int end) { //翻转
while (start < end) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start += 1;
end -= 1;
}
}
}
我们以 n=7n=7,k=3k=3 为例进行如下展示:
操作 结果
原始数组 1 2 3 4 5 6 7
翻转所有元素 7 6 5 4 3 2 1
翻转[0,kmodn−1] 区间的元素 5 6 7 4 3 2 1
翻转 [kmodn,n−1] 区间的元素 5 6 7 1 2 3 4
翻转一遍会让顺序错乱,但翻转两遍等于原来的顺序,所以我们采用先整体翻转,再局部定好我们需要的范围去翻转。
217、难度简单:本题难点:时间有限制,尽量不用两层for循环
class Solution {
public boolean containsDuplicate(int[] nums) {
Arrays.sort(nums);
int n = nums.length;
for (int i = 0; i < n - 1; i++) {
if (nums[i] == nums[i + 1]) {
return true;
}
}
return false;
}
}
原理:在对数字从小到大排序之后,数组的重复元素一定出现在相邻位置中。因此,我们可以扫描已排序的数组,每次判断相邻的两个元素是否相等,如果相等则说明存在重复的元素。