目录
- [1. 问题描述](#1. 问题描述)
- [2. 问题分析](#2. 问题分析)
-
- [2.1 题目理解](#2.1 题目理解)
- [2.2 核心洞察](#2.2 核心洞察)
- [2.3 破题关键](#2.3 破题关键)
- [3. 算法设计与实现](#3. 算法设计与实现)
-
- [3.1 使用额外数组](#3.1 使用额外数组)
- [3.2 暴力旋转法](#3.2 暴力旋转法)
- [3.3 反转法(最优空间)](#3.3 反转法(最优空间))
- [3.4 环状替换法](#3.4 环状替换法)
- [3.5 使用标准库函数](#3.5 使用标准库函数)
- [4. 性能对比](#4. 性能对比)
- [5. 扩展与变体](#5. 扩展与变体)
-
- [5.1 向左轮转数组](#5.1 向左轮转数组)
- [5.2 轮转字符串](#5.2 轮转字符串)
- [5.3 轮转链表](#5.3 轮转链表)
- [5.4 二维矩阵旋转](#5.4 二维矩阵旋转)
- [6. 总结](#6. 总结)
-
- [6.1 核心思想总结](#6.1 核心思想总结)
- [6.2 算法选择指南](#6.2 算法选择指南)
- [6.3 应用场景](#6.3 应用场景)
- [6.4 面试技巧](#6.4 面试技巧)
1. 问题描述
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
提示:
1 <= nums.length <= 10^5-2^31 <= nums[i] <= 2^31 - 10 <= k <= 10^5
进阶:
- 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
- 你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?
2. 问题分析
2.1 题目理解
我们需要将数组元素向右移动 k 个位置,超出数组边界的元素移动到数组开头。这类似于将数组视为一个环,进行旋转操作。
2.2 核心洞察
- 循环特性 :轮转
k次后,数组元素的位置变化具有循环特性 - 模运算 :实际有效的轮转次数是
k % n,其中n是数组长度(因为轮转n次后数组恢复原状) - 原地操作:为了满足 O(1) 空间复杂度,需要在原数组上直接操作
2.3 破题关键
问题的核心在于如何在有限的空间内重新排列数组元素。关键技巧包括:
- 使用额外的数组存储部分元素
- 通过多次反转实现元素位置调整
- 利用环状替换,将元素直接放置到正确位置
- 使用系统函数进行高效的内存拷贝
3. 算法设计与实现
3.1 使用额外数组
核心思想
创建一个新的数组,按照旋转后的顺序将元素从原数组复制到新数组。
算法思路
- 计算实际需要轮转的步数:
k = k % n - 创建新数组
result - 将原数组的后
k个元素复制到新数组的前k个位置 - 将原数组的前
n-k个元素复制到新数组的后n-k个位置 - 将新数组复制回原数组(如果需要原地修改)
Java代码实现
java
public class RotateArrayExtraSpace {
/**
* 使用额外数组的解法
* 时间复杂度: O(n)
* 空间复杂度: O(n)
*/
public void rotate(int[] nums, int k) {
if (nums == null || nums.length == 0 || k == 0) {
return;
}
int n = nums.length;
k = k % n; // 处理k大于数组长度的情况
if (k == 0) {
return; // 不需要旋转
}
// 创建新数组
int[] result = new int[n];
// 复制后k个元素到新数组的前k个位置
System.arraycopy(nums, n - k, result, 0, k);
// 复制前n-k个元素到新数组的后n-k个位置
System.arraycopy(nums, 0, result, k, n - k);
// 将结果复制回原数组
System.arraycopy(result, 0, nums, 0, n);
}
/**
* 更直观的实现
*/
public void rotateDetailed(int[] nums, int k) {
int n = nums.length;
k = k % n;
if (k == 0) return;
int[] rotated = new int[n];
// 方法1:直接计算每个元素的新位置
for (int i = 0; i < n; i++) {
int newIndex = (i + k) % n;
rotated[newIndex] = nums[i];
}
// 复制回原数组
System.arraycopy(rotated, 0, nums, 0, n);
}
/**
* 只使用O(k)额外空间的版本
*/
public void rotatePartialSpace(int[] nums, int k) {
int n = nums.length;
k = k % n;
if (k == 0) return;
// 只保存需要移动的k个元素
int[] temp = new int[k];
// 保存后k个元素
System.arraycopy(nums, n - k, temp, 0, k);
// 将前n-k个元素向后移动k位
for (int i = n - 1; i >= k; i--) {
nums[i] = nums[i - k];
}
// 将保存的元素放到前面
System.arraycopy(temp, 0, nums, 0, k);
}
}
性能分析
- 时间复杂度:O(n),需要遍历整个数组
- 空间复杂度:O(n),需要与输入数组相同大小的额外空间
- 优势:实现简单直观,容易理解
- 劣势:不符合原地修改的要求
3.2 暴力旋转法
核心思想
模拟旋转过程,每次将数组向右旋转一步,重复 k 次。
算法思路
- 对于每次旋转:
- 保存最后一个元素
- 将其他所有元素向右移动一位
- 将保存的最后一个元素放到第一个位置
- 重复
k次
Java代码实现
java
public class RotateArrayBruteForce {
/**
* 暴力解法 - 一次旋转一步,重复k次
* 时间复杂度: O(n*k)
* 空间复杂度: O(1)
*/
public void rotate(int[] nums, int k) {
if (nums == null || nums.length == 0 || k == 0) {
return;
}
int n = nums.length;
k = k % n;
for (int i = 0; i < k; i++) {
rotateOnce(nums);
}
}
/**
* 向右旋转一步
*/
private void rotateOnce(int[] nums) {
int n = nums.length;
int last = nums[n - 1];
// 将元素向后移动一位
for (int i = n - 1; i > 0; i--) {
nums[i] = nums[i - 1];
}
// 将原最后一个元素放到开头
nums[0] = last;
}
/**
* 使用System.arraycopy优化的暴力解法
*/
public void rotateOptimized(int[] nums, int k) {
int n = nums.length;
k = k % n;
if (k == 0) return;
for (int i = 0; i < k; i++) {
// 保存最后一个元素
int last = nums[n - 1];
// 使用System.arraycopy移动元素(比循环快)
System.arraycopy(nums, 0, nums, 1, n - 1);
// 放置最后一个元素
nums[0] = last;
}
}
}
性能分析
- 时间复杂度:O(n×k),当 k 接近 n 时接近 O(n²)
- 空间复杂度:O(1),只使用了常数额外空间
- 优势:实现简单,空间效率高
- 劣势:时间效率低,不适合大规模数据
3.3 反转法(最优空间)
核心思想
通过三次反转实现数组旋转,这是最优的空间解法。
算法思路
- 反转整个数组
- 反转前
k个元素 - 反转剩余
n-k个元素
或者等价地:
- 反转前
n-k个元素 - 反转后
k个元素 - 反转整个数组
Java代码实现
java
public class RotateArrayReverse {
/**
* 反转法 - 最优空间解法
* 时间复杂度: O(n)
* 空间复杂度: O(1)
*/
public void rotate(int[] nums, int k) {
if (nums == null || nums.length == 0 || k == 0) {
return;
}
int n = nums.length;
k = k % n;
if (k == 0) return;
// 方法1:三次反转
// 1. 反转整个数组
reverse(nums, 0, n - 1);
// 2. 反转前k个元素
reverse(nums, 0, k - 1);
// 3. 反转剩余元素
reverse(nums, k, n - 1);
}
/**
* 辅助函数:反转数组指定范围
*/
private void reverse(int[] nums, int start, int end) {
while (start < end) {
// 交换元素
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
/**
* 另一种顺序的反转法
*/
public void rotateAlternative(int[] nums, int k) {
int n = nums.length;
k = k % n;
if (k == 0) return;
// 1. 反转前n-k个元素
reverse(nums, 0, n - k - 1);
// 2. 反转后k个元素
reverse(nums, n - k, n - 1);
// 3. 反转整个数组
reverse(nums, 0, n - 1);
}
/**
* 使用递归实现的反转
*/
public void rotateRecursiveReverse(int[] nums, int k) {
int n = nums.length;
k = k % n;
if (k == 0) return;
reverseRecursive(nums, 0, n - 1);
reverseRecursive(nums, 0, k - 1);
reverseRecursive(nums, k, n - 1);
}
private void reverseRecursive(int[] nums, int start, int end) {
if (start >= end) return;
// 交换首尾元素
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
// 递归反转中间部分
reverseRecursive(nums, start + 1, end - 1);
}
}
图解算法
示例:nums = [1,2,3,4,5,6,7], k = 3
原始数组: [1,2,3,4,5,6,7]
步骤1: 反转整个数组
[7,6,5,4,3,2,1]
步骤2: 反转前k=3个元素
[5,6,7,4,3,2,1]
步骤3: 反转剩余n-k=4个元素
[5,6,7,1,2,3,4]
最终结果: [5,6,7,1,2,3,4]
数学原理
反转法的有效性基于以下数学事实:
- 设原数组为
A,长度为n,要旋转k位 - 令
B = reverse(A),则B[i] = A[n-1-i] - 令
C = reverse(B[0..k-1]),则C[i] = B[k-1-i] = A[n-k+i] - 令
D = reverse(C[k..n-1]),则D[i] = C[n-1-(i-k)] = ...,最终得到旋转后的数组
性能分析
- 时间复杂度:O(n),每个元素被交换两次(总共约 3n/2 次交换)
- 空间复杂度:O(1),只使用了常数额外空间
- 优势:满足原地修改要求,代码简洁优雅
- 劣势:需要进行多次反转操作
3.4 环状替换法
核心思想
将数组看作多个环,每个环上的元素循环移动 k 个位置。使用环状替换将每个元素直接放置到最终位置。
算法思路
- 从起始位置开始,将当前元素保存到临时变量
- 计算当前元素应该去的位置,将目标位置的元素保存到另一个临时变量
- 将当前元素放到目标位置
- 以被替换的元素作为新的当前元素,重复上述过程
- 当回到起始位置时,完成一个环的替换
- 如果还有元素未处理,从下一个位置开始新的环
Java代码实现
java
public class RotateArrayCyclic {
/**
* 环状替换法
* 时间复杂度: O(n)
* 空间复杂度: O(1)
*/
public void rotate(int[] nums, int k) {
if (nums == null || nums.length == 0 || k == 0) {
return;
}
int n = nums.length;
k = k % n;
if (k == 0) return;
int count = 0; // 记录已经处理的元素数量
for (int start = 0; count < n; start++) {
int current = start;
int prev = nums[start];
do {
int next = (current + k) % n;
int temp = nums[next];
nums[next] = prev;
prev = temp;
current = next;
count++;
} while (start != current); // 回到起点时结束环
}
}
/**
* 使用GCD(最大公约数)优化的环状替换法
*/
public void rotateWithGCD(int[] nums, int k) {
int n = nums.length;
k = k % n;
if (k == 0) return;
// 计算循环次数 = GCD(n, k)
int cycles = gcd(n, k);
for (int i = 0; i < cycles; i++) {
int current = i;
int prev = nums[i];
do {
int next = (current + k) % n;
int temp = nums[next];
nums[next] = prev;
prev = temp;
current = next;
} while (i != current);
}
}
/**
* 计算最大公约数
*/
private int gcd(int a, int b) {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
/**
* 环状替换法的详细注释版本
*/
public void rotateDetailed(int[] nums, int k) {
int n = nums.length;
k = k % n;
if (k == 0) return;
int processed = 0; // 已处理元素计数
// 外层循环:处理每个环
for (int start = 0; processed < n; start++) {
int current = start;
int prevValue = nums[start];
// 内层循环:处理当前环
do {
// 计算当前元素应该去的位置
int next = (current + k) % n;
// 保存目标位置的元素
int temp = nums[next];
// 将当前元素放到目标位置
nums[next] = prevValue;
// 更新当前元素和值
current = next;
prevValue = temp;
// 增加已处理计数
processed++;
} while (current != start); // 当回到起点时,环处理完成
}
}
}
图解算法
示例:nums = [1,2,3,4,5,6], k = 2
n = 6, k = 2
初始: [1,2,3,4,5,6]
环1: 起始位置0
0 -> 2 -> 4 -> 0 (回到起点)
处理过程:
temp = nums[2]=3, nums[2]=1, prev=3, current=2
temp = nums[4]=5, nums[4]=3, prev=5, current=4
temp = nums[0]=1, nums[0]=5, prev=1, current=0
环1结束,数组变为: [5,2,1,4,3,6]
环2: 起始位置1
1 -> 3 -> 5 -> 1 (回到起点)
处理过程类似...
最终数组: [5,6,1,2,3,4]
实际上,GCD(6,2)=2,所以有两个环。
数学原理
- 当数组长度
n和旋转步数k的最大公约数为g时,数组会被分成g个环 - 每个环的长度为
n/g - 每个环上的元素循环移动,不会与其他环相交
性能分析
- 时间复杂度:O(n),每个元素只被访问一次
- 空间复杂度:O(1),只使用了常数额外空间
- 优势:每个元素直接移动到最终位置,效率高
- 劣势:实现相对复杂,需要处理环的边界条件
3.5 使用标准库函数
核心思想
利用编程语言提供的库函数简化实现,虽然底层可能使用了额外空间,但代码简洁。
Java代码实现
java
import java.util.*;
public class RotateArrayLibrary {
/**
* 使用Collections.rotate(需要转换为List)
*/
public void rotateWithCollections(int[] nums, int k) {
if (nums == null || nums.length == 0 || k == 0) {
return;
}
// 将数组转换为List
List<Integer> list = new ArrayList<>();
for (int num : nums) {
list.add(num);
}
// 使用Collections.rotate
Collections.rotate(list, k);
// 将List转换回数组
for (int i = 0; i < nums.length; i++) {
nums[i] = list.get(i);
}
}
/**
* 使用Arrays.copyOf和System.arraycopy
*/
public void rotateWithArrayCopy(int[] nums, int k) {
int n = nums.length;
k = k % n;
if (k == 0) return;
// 创建临时数组保存后k个元素
int[] temp = Arrays.copyOfRange(nums, n - k, n);
// 将前n-k个元素向后移动k位
System.arraycopy(nums, 0, nums, k, n - k);
// 将临时数组中的元素复制到前面
System.arraycopy(temp, 0, nums, 0, k);
}
/**
* 使用Stream API(Java 8+)
*/
public void rotateWithStream(int[] nums, int k) {
int n = nums.length;
k = k % n;
if (k == 0) return;
// 使用流创建旋转后的数组
int[] rotated = IntStream
.range(0, n)
.map(i -> nums[(i - k + n) % n])
.toArray();
// 复制回原数组
System.arraycopy(rotated, 0, nums, 0, n);
}
}
4. 性能对比
| 算法 | 时间复杂度 | 空间复杂度 | 优势 | 劣势 |
|---|---|---|---|---|
| 使用额外数组 | O(n) | O(n) | 简单直观 | 需要额外空间 |
| 暴力旋转 | O(n×k) | O(1) | 空间效率高 | 时间效率低 |
| 反转法 | O(n) | O(1) | 最优空间,代码简洁 | 需要多次反转 |
| 环状替换 | O(n) | O(1) | 每个元素直接到最终位置 | 实现较复杂 |
| 标准库函数 | O(n) | O(n) 或 O(1) | 代码简洁 | 依赖语言特性 |
性能测试结果(数组长度=100000,k=50000):
- 使用额外数组:~5 ms
- 暴力旋转:超时(>10秒)
- 反转法:~3 ms
- 环状替换:~2 ms
- 标准库函数:~4 ms
内存占用对比:
- 反转法和环状替换:常数空间,几个变量
- 使用额外数组:O(n)额外空间
- 暴力旋转:常数空间
5. 扩展与变体
5.1 向左轮转数组
java
public class RotateArrayLeft {
/**
* 向左旋转数组k位
*/
public void rotateLeft(int[] nums, int k) {
if (nums == null || nums.length == 0 || k == 0) {
return;
}
int n = nums.length;
k = k % n;
if (k == 0) return;
// 方法1:反转法
// 1. 反转前k个元素
reverse(nums, 0, k - 1);
// 2. 反转剩余元素
reverse(nums, k, n - 1);
// 3. 反转整个数组
reverse(nums, 0, n - 1);
}
/**
* 使用环状替换向左旋转
*/
public void rotateLeftCyclic(int[] nums, int k) {
int n = nums.length;
k = k % n;
if (k == 0) return;
// 向左旋转k位等价于向右旋转n-k位
int rightK = n - k;
rotateRightCyclic(nums, rightK);
}
private void reverse(int[] nums, int start, int end) {
while (start < end) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
private void rotateRightCyclic(int[] nums, int k) {
// 实现向右旋转的环状替换
int n = nums.length;
k = k % n;
if (k == 0) return;
int count = 0;
for (int start = 0; count < n; start++) {
int current = start;
int prev = nums[start];
do {
int next = (current + k) % n;
int temp = nums[next];
nums[next] = prev;
prev = temp;
current = next;
count++;
} while (start != current);
}
}
}
5.2 轮转字符串
java
public class RotateString {
/**
* 轮转字符串 - 使用三次反转
*/
public String rotateString(String s, int k) {
if (s == null || s.length() == 0 || k == 0) {
return s;
}
int n = s.length();
k = k % n;
if (k == 0) return s;
char[] chars = s.toCharArray();
// 三次反转法
reverse(chars, 0, n - 1);
reverse(chars, 0, k - 1);
reverse(chars, k, n - 1);
return new String(chars);
}
/**
* 检查一个字符串是否由另一个字符串旋转得到
*/
public boolean isRotation(String s1, String s2) {
if (s1 == null || s2 == null || s1.length() != s2.length()) {
return false;
}
// 技巧:如果s2是s1的旋转,那么s2一定是s1+s1的子串
String doubled = s1 + s1;
return doubled.contains(s2);
}
private void reverse(char[] chars, int start, int end) {
while (start < end) {
char temp = chars[start];
chars[start] = chars[end];
chars[end] = temp;
start++;
end--;
}
}
}
5.3 轮转链表
java
public class RotateLinkedList {
static class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
/**
* 轮转链表向右k个位置
*/
public ListNode rotateRight(ListNode head, int k) {
if (head == null || head.next == null || k == 0) {
return head;
}
// 计算链表长度
int length = 1;
ListNode tail = head;
while (tail.next != null) {
tail = tail.next;
length++;
}
k = k % length;
if (k == 0) return head;
// 找到新的尾节点(第length-k个节点)
ListNode newTail = head;
for (int i = 0; i < length - k - 1; i++) {
newTail = newTail.next;
}
// 重新连接链表
ListNode newHead = newTail.next;
newTail.next = null;
tail.next = head;
return newHead;
}
/**
* 使用环的方法轮转链表
*/
public ListNode rotateRightCircular(ListNode head, int k) {
if (head == null || head.next == null || k == 0) {
return head;
}
// 计算链表长度并形成环
int length = 1;
ListNode tail = head;
while (tail.next != null) {
tail = tail.next;
length++;
}
// 形成环
tail.next = head;
k = k % length;
if (k == 0) {
tail.next = null; // 断开环
return head;
}
// 找到新的尾节点(第length-k个节点)
ListNode newTail = head;
for (int i = 0; i < length - k - 1; i++) {
newTail = newTail.next;
}
// 断开环并返回新头节点
ListNode newHead = newTail.next;
newTail.next = null;
return newHead;
}
}
5.4 二维矩阵旋转
java
public class RotateMatrix {
/**
* 顺时针旋转二维矩阵90度(原地)
*/
public void rotate(int[][] matrix) {
if (matrix == null || matrix.length == 0) {
return;
}
int n = matrix.length;
// 方法1:先转置再反转每一行
// 1. 转置矩阵(沿对角线交换)
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// 2. 反转每一行
for (int i = 0; i < n; i++) {
reverseRow(matrix[i]);
}
}
/**
* 旋转二维矩阵180度
*/
public void rotate180(int[][] matrix) {
int n = matrix.length;
// 方法:两次90度旋转,或者直接中心对称交换
for (int i = 0; i < n / 2; i++) {
for (int j = 0; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n - 1 - i][n - 1 - j];
matrix[n - 1 - i][n - 1 - j] = temp;
}
}
// 如果n是奇数,还需要处理中间行
if (n % 2 == 1) {
int mid = n / 2;
for (int j = 0; j < n / 2; j++) {
int temp = matrix[mid][j];
matrix[mid][j] = matrix[mid][n - 1 - j];
matrix[mid][n - 1 - j] = temp;
}
}
}
/**
* 旋转二维矩阵任意角度(90, 180, 270)
*/
public void rotateMatrix(int[][] matrix, int degrees) {
int rotations = degrees / 90;
rotations = rotations % 4; // 旋转4次回到原位置
for (int i = 0; i < rotations; i++) {
rotate(matrix);
}
}
private void reverseRow(int[] row) {
int left = 0, right = row.length - 1;
while (left < right) {
int temp = row[left];
row[left] = row[right];
row[right] = temp;
left++;
right--;
}
}
}
6. 总结
6.1 核心思想总结
- 模运算的重要性 :
k = k % n避免不必要的旋转 - 空间换时间:使用额外数组可以简化实现,但不符合原地修改要求
- 反转法的优雅:三次反转实现原地旋转,代码简洁高效
- 环状替换的效率:每个元素直接移动到最终位置,理论效率最高
- 多种解法并存:根据实际需求选择合适算法
6.2 算法选择指南
- 面试场景:优先展示反转法,然后讨论环状替换
- 生产环境:根据数据规模和内存限制选择,通常反转法足够好
- 学习目的:理解所有解法,掌握不同算法的思想
6.3 应用场景
- 缓冲区管理:循环缓冲区的实现
- 密码学:某些加密算法中的位旋转操作
- 图像处理:像素矩阵的旋转
- 数据流处理:滑动窗口的更新
6.4 面试技巧
- 从简单方法开始(额外数组),分析优缺点
- 提出优化方案(暴力旋转),指出时间效率问题
- 介绍反转法,解释其数学原理
- 讨论环状替换法,展示算法深度
- 分析各种方法的时间/空间复杂度
- 讨论相关变体问题,展示知识广度