每日一题-旋转链表🟡
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
示例 1:

输入:head = [1,2,3,4,5], k = 2 输出:[4,5,1,2,3]
示例 2:

输入:head = [0,1,2], k = 4 输出:[2,0,1]
提示:
- 链表中节点的数目在范围
[0, 500]内 -100 <= Node.val <= 1000 <= k <= 2 * 109
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
示例 1:

输入:head = [1,2,3,4,5], k = 2 输出:[4,5,1,2,3]
示例 2:

输入:head = [0,1,2], k = 4 输出:[2,0,1]
提示:
[0, 500] 内-100 <= Node.val <= 1000 <= k <= 2 * 109
示例 1 的链表长为 $5$,$k=2$。旋转后,原链表的倒数第 $k$ 个节点,成为新链表的头节点。
把 $1\to 2\to 3\to 4\to 5$ 变成 $4\to 5\to 1\to 2\to 3$,我们需要:
本题 $k$ 可能很大,我们需要先求出链表的长度 $n$,然后把 $k$ 更新为 $k\bmod n$。这是因为链表旋转 $n$ 次没变,旋转 $n+1$ 次等同于旋转 $1$ 次,依此类推,旋转 $k$ 次等价于旋转 $k\bmod n$ 次。
倒数第 $k+1$ 个节点即正数第 $n-k$ 个节点。从头节点开始,向后移动 $n-k-1$ 次,即为正数第 $n-k$ 个节点。
###py
class Solution:
def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
if head is None:
return None
# 1. 计算链表长度,并找到尾节点
length = 1
tail = head
while tail.next:
length += 1
tail = tail.next
k %= length
# 2. 首尾相连
tail.next = head
# 3. 找倒数第 k+1 个节点,作为新链表的尾节点
new_tail = head
for _ in range(length - k - 1):
new_tail = new_tail.next
# 4. 断开倒数第 k+1 个节点(new_tail)和倒数第 k 个节点(new_head)
new_head = new_tail.next
new_tail.next = None
return new_head
###java
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if (head == null) {
return null;
}
// 1. 计算链表长度,并找到尾节点
int length = 1;
ListNode tail = head;
while (tail.next != null) {
length++;
tail = tail.next;
}
k %= length;
// 2. 首尾相连
tail.next = head;
// 3. 找倒数第 k+1 个节点,作为新链表的尾节点
ListNode newTail = head;
for (int i = 0; i < length - k - 1; i++) {
newTail = newTail.next;
}
// 4. 断开倒数第 k+1 个节点(newTail)和倒数第 k 个节点(newHead)
ListNode newHead = newTail.next;
newTail.next = null;
return newHead;
}
}
###cpp
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if (head == nullptr) {
return nullptr;
}
// 1. 计算链表长度,并找到尾节点
int length = 1;
ListNode* tail = head;
while (tail->next) {
length++;
tail = tail->next;
}
k %= length;
// 2. 首尾相连
tail->next = head;
// 3. 找倒数第 k+1 个节点,作为新链表的尾节点
ListNode* new_tail = head;
for (int i = 0; i < length - k - 1; i++) {
new_tail = new_tail->next;
}
// 4. 断开倒数第 k+1 个节点(new_tail)和倒数第 k 个节点(new_head)
ListNode* new_head = new_tail->next;
new_tail->next = nullptr;
return new_head;
}
};
###c
struct ListNode* rotateRight(struct ListNode* head, int k) {
if (head == NULL) {
return NULL;
}
// 1. 计算链表长度,并找到尾节点
int length = 1;
struct ListNode* tail = head;
while (tail->next) {
length++;
tail = tail->next;
}
k %= length;
// 2. 首尾相连
tail->next = head;
// 3. 找倒数第 k+1 个节点,作为新链表的尾节点
struct ListNode* new_tail = head;
for (int i = 0; i < length - k - 1; i++) {
new_tail = new_tail->next;
}
// 4. 断开倒数第 k+1 个节点(new_tail)和倒数第 k 个节点(new_head)
struct ListNode* new_head = new_tail->next;
new_tail->next = NULL;
return new_head;
}
###go
func rotateRight(head *ListNode, k int) *ListNode {
if head == nil {
return nil
}
// 1. 计算链表长度,并找到尾节点
length := 1
tail := head
for tail.Next != nil {
length++
tail = tail.Next
}
k %= length
// 2. 首尾相连
tail.Next = head
// 3. 找倒数第 k+1 个节点,作为新链表的尾节点
newTail := head
for range length - k - 1 {
newTail = newTail.Next
}
// 4. 断开倒数第 k+1 个节点(newTail)和倒数第 k 个节点(newHead)
newHead := newTail.Next
newTail.Next = nil
return newHead
}
###js
var rotateRight = function(head, k) {
if (head === null) {
return null;
}
// 1. 计算链表长度,并找到尾节点
let length = 1;
let tail = head;
while (tail.next !== null) {
length++;
tail = tail.next;
}
k %= length;
// 2. 首尾相连
tail.next = head;
// 3. 找倒数第 k+1 个节点,作为新链表的尾节点
let newTail = head;
for (let i = 0; i < length - k - 1; i++) {
newTail = newTail.next;
}
// 4. 断开倒数第 k+1 个节点(newTail)和倒数第 k 个节点(newHead)
const newHead = newTail.next;
newTail.next = null;
return newHead;
};
###rust
impl Solution {
pub fn rotate_right(mut head: Option<Box<ListNode>>, mut k: i32) -> Option<Box<ListNode>> {
if head.is_none() {
return head;
}
// 1. 计算链表长度
let mut length = 0;
let mut cur = &head;
while let Some(node) = cur {
length += 1;
cur = &node.next;
}
k %= length;
if k == 0 { // 链表不变
return head;
}
// 2. 找倒数第 k 个节点
let mut cur = &mut head;
for _ in 0..length - k {
cur = &mut cur.as_mut()?.next;
}
// 3. 断开倒数第 k+1 个节点和倒数第 k 个节点(new_head)
let mut new_head = cur.take();
// 4. 首尾相连
let mut tail = &mut new_head;
while !tail.as_mut()?.next.is_none() {
tail = &mut tail.as_mut()?.next;
}
tail.as_mut()?.next = head;
new_head
}
}
欢迎关注 B站@灵茶山艾府
(模拟) $O(n)$
给你一个链表的头节点 head ,然后将链表每个节点向右移动 k 个位置。
样例:
{:width="70%"}
如样例所示,head = [1,2,3,4,5],k = 2,我们输出[4,5,1,2,3]。下面来讲解模拟的做法。
假设链表的长度为n,为了将链表每个节点向右移动 k 个位置,我们只需要将链表的后 k % n个节点移动到链表的最前面,然后将链表的后k % n个节点和前 n - k个节点连接到一块即可。
具体过程如下:
1、首先遍历整个链表,求出链表的长度n,并找出链表的尾节点tail。
{:width="70%"}
2、由于k可能很大,所以我们令 k = k % n,然后再次从头节点head开始遍历,找到第n - k个节点p,那么1 ~ p是链表的前 n - k个节点,p+1 ~ n是链表的后k个节点。
{:width="70%"}
3、接下来就是依次执行 tail->next = head,head = p->next,p->next = nullptr,将链表的后k个节点和前 n - k个节点拼接到一块,并让head指向新的头节点(p->next),新的尾节点即p节点的next指针指向null。
{:width="70%"}
4、最后返回链表的新的头节点head。
时间复杂度分析: 链表一共被遍历两次,因此总的时间复杂度为$O(n)$,$n$是链表的长度。
###c
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if(!head || !k) return head;
int n = 0; //链表的长度
ListNode* tail; //尾节点
for(ListNode* p = head; p ; p = p->next){
tail = p;
n++;
}
k %= n;
ListNode* p = head;
for(int i = 0; i < n - k - 1; i++) p = p->next; //找到链表的第n-k个节点
tail->next = head;
head = p->next;
p->next = nullptr;
return head; //返回新的头节点
}
};
###javascript
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if(head == null|| k == 0) return head;
int n = 0; //链表的长度
ListNode tail = null; //尾节点
for(ListNode p = head; p != null ; p = p.next){
tail = p;
n++;
}
k %= n;
ListNode p = head;
for(int i = 0; i < n - k - 1; i++) p = p.next; //找到链表的第n-k个节点
tail.next = head;
head = p.next;
p.next = null;
return head; //返回新的头节点
}
}
思路及算法
记给定链表的长度为 $n$,注意到当向右移动的次数 $k \geq n$ 时,我们仅需要向右移动 $k \bmod n$ 次即可。因为每 $n$ 次移动都会让链表变为原状。这样我们可以知道,新链表的最后一个节点为原链表的第 $(n - 1) - (k \bmod n)$ 个节点(从 $0$ 开始计数)。
这样,我们可以先将给定的链表连接成环,然后将指定位置断开。
具体代码中,我们首先计算出链表的长度 $n$,并找到该链表的末尾节点,将其与头节点相连。这样就得到了闭合为环的链表。然后我们找到新链表的最后一个节点(即原链表的第 $(n - 1) - (k \bmod n)$ 个节点),将当前闭合为环的链表断开,即可得到我们所需要的结果。
特别地,当链表长度不大于 $1$,或者 $k$ 为 $n$ 的倍数时,新链表将与原链表相同,我们无需进行任何处理。
代码
###C++
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if (k == 0 || head == nullptr || head->next == nullptr) {
return head;
}
int n = 1;
ListNode* iter = head;
while (iter->next != nullptr) {
iter = iter->next;
n++;
}
int add = n - k % n;
if (add == n) {
return head;
}
iter->next = head;
while (add--) {
iter = iter->next;
}
ListNode* ret = iter->next;
iter->next = nullptr;
return ret;
}
};
###Java
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if (k == 0 || head == null || head.next == null) {
return head;
}
int n = 1;
ListNode iter = head;
while (iter.next != null) {
iter = iter.next;
n++;
}
int add = n - k % n;
if (add == n) {
return head;
}
iter.next = head;
while (add-- > 0) {
iter = iter.next;
}
ListNode ret = iter.next;
iter.next = null;
return ret;
}
}
###Python
class Solution:
def rotateRight(self, head: ListNode, k: int) -> ListNode:
if k == 0 or not head or not head.next:
return head
n = 1
cur = head
while cur.next:
cur = cur.next
n += 1
if (add := n - k % n) == n:
return head
cur.next = head
while add:
cur = cur.next
add -= 1
ret = cur.next
cur.next = None
return ret
###JavaScript
var rotateRight = function(head, k) {
if (k === 0 || !head || !head.next) {
return head;
}
let n = 1;
let cur = head;
while (cur.next) {
cur = cur.next;
n++;
}
let add = n - k % n;
if (add === n) {
return head;
}
cur.next = head;
while (add) {
cur = cur.next;
add--;
}
const ret = cur.next;
cur.next = null;
return ret;
};
###go
func rotateRight(head *ListNode, k int) *ListNode {
if k == 0 || head == nil || head.Next == nil {
return head
}
n := 1
iter := head
for iter.Next != nil {
iter = iter.Next
n++
}
add := n - k%n
if add == n {
return head
}
iter.Next = head
for add > 0 {
iter = iter.Next
add--
}
ret := iter.Next
iter.Next = nil
return ret
}
###C
struct ListNode* rotateRight(struct ListNode* head, int k) {
if (k == 0 || head == NULL || head->next == NULL) {
return head;
}
int n = 1;
struct ListNode* iter = head;
while (iter->next != NULL) {
iter = iter->next;
n++;
}
int add = n - k % n;
if (add == n) {
return head;
}
iter->next = head;
while (add--) {
iter = iter->next;
}
struct ListNode* ret = iter->next;
iter->next = NULL;
return ret;
}
复杂度分析
时间复杂度:$O(n)$,最坏情况下,我们需要遍历该链表两次。
空间复杂度:$O(1)$,我们只需要常数的空间存储若干变量。
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:

输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]] 输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
提示:
n == matrix.length == matrix[i].length1 <= n <= 20-1000 <= matrix[i][j] <= 1000
把一个方阵($n\times n$ 的矩阵)顺时针旋转 $90^\circ$。
要求:不能创建另一个矩阵,空间复杂度必须是 $\mathcal{O}(1)$。

顺时针旋转 $90^\circ$ 后,位于 $(i,j)$ 的元素去哪了?
竖着看:
横着看:
所以位于 $i$ 行 $j$ 列的元素,去到 $j$ 行 $n-1-i$ 列,即 $(i,j)\to (j,n-1-i)$。
$(i,j)\to (j,n-1-i)$ 可以通过两次翻转操作得到:
$$
(i,j)\xrightarrow{转置} (j,i) \xrightarrow{行翻转} (j,n-1-i)
$$
示例 1 的操作过程如下:
$$
\begin{bmatrix}
1 & 2 & 3 \
4 & 5 & 6 \
7 & 8 & 9 \
\end{bmatrix}
\xrightarrow{转置}
\begin{bmatrix}
1 & 4 & 7 \
2 & 5 & 8 \
3 & 6 & 9 \
\end{bmatrix}
\xrightarrow{行翻转}
\begin{bmatrix}
7 & 4 & 1 \
8 & 5 & 2 \
9 & 6 & 3 \
\end{bmatrix}
$$
注:一般地,把一个点绕 $O$ 旋转任意 $\theta$ 角度,可以通过两个翻转操作实现。要求这两条翻转的对称轴,交点为 $O$ 且夹角为 $\dfrac{\theta}{2}$。对于本题,每个元素需要绕矩阵中心顺时针旋转 $90^\circ$,这可以通过关于主对角线翻转,关于垂直中轴翻转实现。这两条对称轴的交点为矩阵中心,且夹角为 $45^\circ$。
这两步操作都可以原地实现。
###py
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
# 第一步:转置
for i in range(n):
for j in range(i): # 遍历对角线下方元素
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
# 第二步:行翻转
for row in matrix:
row.reverse()
###java
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 第一步:转置
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) { // 遍历对角线下方元素
int tmp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = tmp;
}
}
// 第二步:行翻转
for (int[] row : matrix) {
for (int j = 0; j < n / 2; j++) { // 遍历左半元素
int tmp = row[j];
row[j] = row[n - 1 - j];
row[n - 1 - j] = tmp;
}
}
}
}
###cpp
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 第一步:转置
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) { // 遍历对角线下方元素
swap(matrix[i][j], matrix[j][i]);
}
}
// 第二步:行翻转
for (auto& row : matrix) {
ranges::reverse(row);
}
}
};
###c
#define SWAP(a, b) do { int tmp = (a); (a) = (b); (b) = tmp; } while (0)
void rotate(int** matrix, int matrixSize, int* matrixColSize) {
int n = matrixSize;
// 第一步:转置
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) { // 遍历对角线下方元素
SWAP(matrix[i][j], matrix[j][i]);
}
}
// 第二步:行翻转
for (int i = 0; i < n; i++) {
int* row = matrix[i];
for (int j = 0; j < n / 2; j++) { // 遍历左半元素
SWAP(row[j], row[n - 1 - j]);
}
}
}
###go
func rotate(matrix [][]int) {
n := len(matrix)
// 第一步:转置
for i := range n {
for j := range i { // 遍历对角线下方元素
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
}
}
// 第二步:行翻转
for _, row := range matrix {
slices.Reverse(row)
}
}
###js
var rotate = function(matrix) {
const n = matrix.length;
// 第一步:转置
for (let i = 0; i < n; i++) {
for (let j = 0; j < i; j++) { // 遍历对角线下方元素
const tmp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = tmp;
}
}
// 第二步:行翻转
for (const row of matrix) {
row.reverse();
}
};
###rust
impl Solution {
pub fn rotate(matrix: &mut Vec<Vec<i32>>) {
let n = matrix.len();
// 第一步:转置
for i in 0..n {
for j in 0..i { // 遍历对角线下方元素
(matrix[i][j], matrix[j][i]) = (matrix[j][i], matrix[i][j]);
}
}
// 第二步:行翻转
for row in matrix {
row.reverse();
}
}
}
把两个循环合并成一个循环。
需要把遍历顺序调整为遍历对角线上方元素,这样每行遍历完后,这一行的元素后面不会再访问到,可以直接做行翻转。
###py
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
for i, row in enumerate(matrix):
for j in range(i + 1, n): # 遍历对角线上方元素,做转置
row[j], matrix[j][i] = matrix[j][i], row[j]
row.reverse() # 行翻转
###java
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
for (int i = 0; i < n; i++) {
int[] row = matrix[i];
for (int j = i + 1; j < n; j++) { // 遍历对角线上方元素,做转置
int tmp = row[j];
row[j] = matrix[j][i];
matrix[j][i] = tmp;
}
for (int j = 0; j < n / 2; j++) { // 遍历左半元素,做行翻转
int tmp = row[j];
row[j] = row[n - 1 - j];
row[n - 1 - j] = tmp;
}
}
}
}
###cpp
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) { // 遍历对角线上方元素,做转置
swap(matrix[i][j], matrix[j][i]);
}
ranges::reverse(matrix[i]); // 行翻转
}
}
};
###c
#define SWAP(a, b) do { int tmp = (a); (a) = (b); (b) = tmp; } while (0)
void rotate(int** matrix, int matrixSize, int* matrixColSize) {
int n = matrixSize;
for (int i = 0; i < n; i++) {
int* row = matrix[i];
for (int j = i + 1; j < n; j++) { // 遍历对角线上方元素,做转置
SWAP(row[j], matrix[j][i]);
}
for (int j = 0; j < n / 2; j++) { // 遍历左半元素,做行翻转
SWAP(row[j], row[n - 1 - j]);
}
}
}
###go
func rotate(matrix [][]int) {
n := len(matrix)
for i, row := range matrix {
for j := i + 1; j < n; j++ { // 遍历对角线上方元素,做转置
row[j], matrix[j][i] = matrix[j][i], row[j]
}
slices.Reverse(row) // 行翻转
}
}
###js
var rotate = function(matrix) {
const n = matrix.length;
for (let i = 0; i < n; i++) {
const row = matrix[i];
for (let j = i + 1; j < n; j++) { // 遍历对角线上方元素,做转置
const tmp = row[j];
row[j] = matrix[j][i];
matrix[j][i] = tmp;
}
row.reverse(); // 行翻转
}
};
###rust
impl Solution {
pub fn rotate(matrix: &mut Vec<Vec<i32>>) {
let n = matrix.len();
for i in 0..n {
for j in i + 1..n { // 遍历对角线上方元素,做转置
(matrix[i][j], matrix[j][i]) = (matrix[j][i], matrix[i][j]);
}
matrix[i].reverse(); // 行翻转
}
}
}
欢迎在评论区分享你的思路/代码。
欢迎关注 B站@灵茶山艾府
如下图所示,矩阵顺时针旋转 90º 后,可找到以下规律:
因此,对于矩阵任意第 $i$ 行、第 $j$ 列元素 $matrix[i][j]$ ,矩阵旋转 90º 后「元素位置旋转公式」为:
$$
\begin{aligned}
matrix[i][j] & \rightarrow matrix[j][n - 1 - i] \
原索引位置 & \rightarrow 旋转后索引位置
\end{aligned}
$$

根据以上「元素旋转公式」,考虑遍历矩阵,将各元素依次写入到旋转后的索引位置。但仍存在问题:在写入一个元素 $matrix[i][j] \rightarrow matrix[j][n - 1 - i]$ 后,原矩阵元素 $matrix[j][n - 1 - i]$ 就会被覆盖(即丢失),而此丢失的元素就无法被写入到旋转后的索引位置了。
为解决此问题,考虑借助一个「辅助矩阵」暂存原矩阵,通过遍历辅助矩阵所有元素,将各元素填入「原矩阵」旋转后的新索引位置即可。
###Python
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
# 深拷贝 matrix -> tmp
tmp = copy.deepcopy(matrix)
# 根据元素旋转公式,遍历修改原矩阵 matrix 的各元素
for i in range(n):
for j in range(n):
matrix[j][n - 1 - i] = tmp[i][j]
###Java
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 深拷贝 matrix -> tmp
int[][] tmp = new int[n][];
for (int i = 0; i < n; i++)
tmp[i] = matrix[i].clone();
// 根据元素旋转公式,遍历修改原矩阵 matrix 的各元素
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
matrix[j][n - 1 - i] = tmp[i][j];
}
}
}
}
###C++
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 深拷贝 matrix -> tmp
vector<vector<int>> tmp = matrix;
// 根据元素旋转公式,遍历修改原矩阵 matrix 的各元素
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
matrix[j][n - 1 - i] = tmp[i][j];
}
}
}
};
如以上代码所示,遍历矩阵所有元素的时间复杂度为 $O(N^2)$ ;由于借助了一个辅助矩阵,空间复杂度为 $O(N^2)$ 。
考虑不借助辅助矩阵,通过在原矩阵中直接「原地修改」,实现空间复杂度 $O(1)$ 的解法。
以位于矩阵四个角点的元素为例,设矩阵左上角元素 $A$ 、右上角元素 $B$ 、右下角元素 $C$ 、左下角元素 $D$ 。矩阵旋转 90º 后,相当于依次先后执行 $D \rightarrow A$ , $C \rightarrow D$ , $B \rightarrow C$ , $A \rightarrow B$ 修改元素,即如下「首尾相接」的元素旋转操作:
$$
A \leftarrow D \leftarrow C \leftarrow B \leftarrow A
$$

如上图所示,由于第 $1$ 步 $D \rightarrow A$ 已经将 $A$ 覆盖(导致 $A$ 丢失),此丢失导致最后第 $4$ 步 $A \rightarrow B$ 无法赋值。为解决此问题,考虑借助一个「辅助变量 $tmp$ 」预先存储 $A$ ,此时的旋转操作变为:
$$
暂存 \ tmp = A \
A \leftarrow D \leftarrow C \leftarrow B \leftarrow tmp
$$

如上图所示,一轮可以完成矩阵 4 个元素的旋转。因而,只要分别以矩阵左上角 $1/4$ 的各元素为起始点执行以上旋转操作,即可完整实现矩阵旋转。
具体来看,当矩阵大小 $n$ 为偶数时,取前 $\frac{n}{2}$ 行、前 $\frac{n}{2}$ 列的元素为起始点;当矩阵大小 $n$ 为奇数时,取前 $\frac{n}{2}$ 行、前 $\frac{n + 1}{2}$ 列的元素为起始点。
令 $matrix[i][j] = A$ ,根据文章开头的元素旋转公式,可推导得适用于任意起始点的元素旋转操作:
$$
暂存 tmp = matrix[i][j] \
matrix[i][j] \leftarrow matrix[n - 1 - j][i] \leftarrow matrix[n - 1 - i][n - 1 - j] \leftarrow matrix[j][n - 1 - i] \leftarrow tmp
$$
如下图所示,为示例矩阵的算法执行流程。
<
,
,
,
,
,
,
,
,
,
,
,
,
,
>
后三个 Tab 为「代码注释解析」。
###Python
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
for i in range(n // 2):
for j in range((n + 1) // 2):
tmp = matrix[i][j]
matrix[i][j] = matrix[n - 1 - j][i]
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j]
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i]
matrix[j][n - 1 - i] = tmp
###Java
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
for (int i = 0; i < n / 2; i++) {
for (int j = 0; j < (n + 1) / 2; j++) {
int tmp = matrix[i][j];
matrix[i][j] = matrix[n - 1 - j][i];
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j];
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i];
matrix[j][n - 1 - i] = tmp;
}
}
}
}
###C++
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for (int i = 0; i < n / 2; i++) {
for (int j = 0; j < (n + 1) / 2; j++) {
int tmp = matrix[i][j];
matrix[i][j] = matrix[n - 1 - j][i];
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j];
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i];
matrix[j][n - 1 - i] = tmp;
}
}
}
};
###Python
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
# 设矩阵行列数为 n
n = len(matrix)
# 起始点范围为 0 <= i < n // 2 , 0 <= j < (n + 1) // 2
# 其中 '//' 为整数除法
for i in range(n // 2):
for j in range((n + 1) // 2):
# 暂存 A 至 tmp
tmp = matrix[i][j]
# 元素旋转操作 A <- D <- C <- B <- tmp
matrix[i][j] = matrix[n - 1 - j][i]
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j]
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i]
matrix[j][n - 1 - i] = tmp
###Java
class Solution {
public void rotate(int[][] matrix) {
// 设矩阵行列数为 n
int n = matrix.length;
// 起始点范围为 0 <= i < n / 2 , 0 <= j < (n + 1) / 2
// 其中 '/' 为整数除法
for (int i = 0; i < n / 2; i++) {
for (int j = 0; j < (n + 1) / 2; j++) {
// 暂存 A 至 tmp
int tmp = matrix[i][j];
// 元素旋转操作 A <- D <- C <- B <- tmp
matrix[i][j] = matrix[n - 1 - j][i];
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j];
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i];
matrix[j][n - 1 - i] = tmp;
}
}
}
}
###C++
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
// 设矩阵行列数为 n
int n = matrix.size();
// 起始点范围为 0 <= i < n / 2 , 0 <= j < (n + 1) / 2
// 其中 '/' 为整数除法
for (int i = 0; i < n / 2; i++) {
for (int j = 0; j < (n + 1) / 2; j++) {
// 暂存 A 至 tmp
int tmp = matrix[i][j];
// 元素旋转操作 A <- D <- C <- B <- tmp
matrix[i][j] = matrix[n - 1 - j][i];
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j];
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i];
matrix[j][n - 1 - i] = tmp;
}
}
}
};
本学习计划配有代码仓,内含测试样例与数据结构封装,便于本地调试。可前往我的个人主页获取。
我们以题目中的示例二
$$
\begin{bmatrix}
5 & 1 & 9 & 11 \
2 & 4 & 8 & 10 \
13 & 3 & 6 & 7 \
15 & 14 & 12 & 16
\end{bmatrix}
$$
作为例子,分析将图像旋转 90 度之后,这些数字出现在什么位置。
对于矩阵中的第一行而言,在旋转后,它出现在倒数第一列的位置:
$$
\begin{bmatrix}
5 & 1 & 9 & 11 \
\circ & \circ & \circ & \circ \
\circ & \circ & \circ & \circ \
\circ & \circ & \circ & \circ \
\end{bmatrix}
\xRightarrow[]{旋转后}
\begin{bmatrix}
\circ & \circ & \circ & 5 \
\circ & \circ & \circ & 1 \
\circ & \circ & \circ & 9 \
\circ & \circ & \circ & 11
\end{bmatrix}
$$
并且,第一行的第 $x$ 个元素在旋转后恰好是倒数第一列的第 $x$ 个元素。
对于矩阵中的第二行而言,在旋转后,它出现在倒数第二列的位置:
$$
\begin{bmatrix}
\circ & \circ & \circ & \circ \
2 & 4 & 8 & 10 \
\circ & \circ & \circ & \circ \
\circ & \circ & \circ & \circ
\end{bmatrix}
\xRightarrow[]{旋转后}
\begin{bmatrix}
\circ & \circ & 2 & \circ \
\circ & \circ & 4 & \circ \
\circ & \circ & 8 & \circ \
\circ & \circ & 10 & \circ
\end{bmatrix}
$$
对于矩阵中的第三行和第四行同理。这样我们可以得到规律:
对于矩阵中第 $i$ 行的第 $j$ 个元素,在旋转后,它出现在倒数第 $i$ 列的第 $j$ 个位置。
我们将其翻译成代码。由于矩阵中的行列从 $0$ 开始计数,因此对于矩阵中的元素 $\textit{matrix}[\textit{row}][\textit{col}]$,在旋转后,它的新位置为 $\textit{matrix}_\textit{new}[\textit{col}][n - \textit{row} - 1]$。
这样以来,我们使用一个与 $\textit{matrix}$ 大小相同的辅助数组 ${matrix}\textit{new}$,临时存储旋转后的结果。我们遍历 $\textit{matrix}$ 中的每一个元素,根据上述规则将该元素存放到 ${matrix}\textit{new}$ 中对应的位置。在遍历完成之后,再将 ${matrix}_\textit{new}$ 中的结果复制到原数组中即可。
###C++
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// C++ 这里的 = 拷贝是值拷贝,会得到一个新的数组
auto matrix_new = matrix;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
matrix_new[j][n - i - 1] = matrix[i][j];
}
}
// 这里也是值拷贝
matrix = matrix_new;
}
};
###Java
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
int[][] matrix_new = new int[n][n];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
matrix_new[j][n - i - 1] = matrix[i][j];
}
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
matrix[i][j] = matrix_new[i][j];
}
}
}
}
###Python
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
# Python 这里不能 matrix_new = matrix 或 matrix_new = matrix[:] 因为是引用拷贝
matrix_new = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
matrix_new[j][n - i - 1] = matrix[i][j]
# 不能写成 matrix = matrix_new
matrix[:] = matrix_new
###JavaScript
var rotate = function(matrix) {
const n = matrix.length;
const matrix_new = new Array(n).fill(0).map(() => new Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
matrix_new[j][n - i - 1] = matrix[i][j];
}
}
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
matrix[i][j] = matrix_new[i][j];
}
}
};
###Go
func rotate(matrix [][]int) {
n := len(matrix)
tmp := make([][]int, n)
for i := range tmp {
tmp[i] = make([]int, n)
}
for i, row := range matrix {
for j, v := range row {
tmp[j][n-1-i] = v
}
}
copy(matrix, tmp) // 拷贝 tmp 矩阵每行的引用
}
###C
void rotate(int** matrix, int matrixSize, int* matrixColSize) {
int matrix_new[matrixSize][matrixSize];
for (int i = 0; i < matrixSize; i++) {
for (int j = 0; j < matrixSize; j++) {
matrix_new[i][j] = matrix[i][j];
}
}
for (int i = 0; i < matrixSize; ++i) {
for (int j = 0; j < matrixSize; ++j) {
matrix[j][matrixSize - i - 1] = matrix_new[i][j];
}
}
}
复杂度分析
时间复杂度:$O(N^2)$,其中 $N$ 是 $\textit{matrix}$ 的边长。
空间复杂度:$O(N^2)$。我们需要使用一个和 $\textit{matrix}$ 大小相同的辅助数组。
题目中要求我们尝试在不使用额外内存空间的情况下进行矩阵的旋转,也就是说,我们需要「原地旋转」这个矩阵。那么我们如何在方法一的基础上完成原地旋转呢?
我们观察方法一中的关键等式:
$$
\textit{matrix}_\textit{new}[\textit{col}][n - \textit{row} - 1] = \textit{matrix}[\textit{row}][\textit{col}]
$$
它阻止了我们进行原地旋转,这是因为如果我们直接将 $\textit{matrix}[\textit{row}][\textit{col}]$ 放到原矩阵中的目标位置 $\textit{matrix}[\textit{col}][n - \textit{row} - 1]$:
$$
\textit{matrix}[\textit{col}][n - \textit{row} - 1] = \textit{matrix}[\textit{row}][\textit{col}]
$$
原矩阵中的 $\textit{matrix}[\textit{col}][n - \textit{row} - 1]$ 就被覆盖了!这并不是我们想要的结果。因此我们可以考虑用一个临时变量 $\textit{temp}$ 暂存 $\textit{matrix}[\textit{col}][n - \textit{row} - 1]$ 的值,这样虽然 $\textit{matrix}[\textit{col}][n - \textit{row} - 1]$ 被覆盖了,我们还是可以通过 $\textit{temp}$ 获取它原来的值:
$$
\left{
\begin{alignedat}{2}
&\textit{temp} &&= \textit{matrix}[\textit{col}][n - \textit{row} - 1]\
&\textit{matrix}[\textit{col}][n - \textit{row} - 1] &&= \textit{matrix}[\textit{row}][\textit{col}]
\end{alignedat}
\right.
$$
那么 $\textit{matrix}[\textit{col}][n - \textit{row} - 1]$ 经过旋转操作之后会到哪个位置呢?我们还是使用方法一中的关键等式,不过这次,我们需要将
$$
\left{
\begin{alignedat}{2}
& \textit{row} &&= \textit{col} \
& \textit{col} &&= n - \textit{row} - 1
\end{alignedat}
\right.
$$
带入关键等式,就可以得到:
$$
\textit{matrix}[n - \textit{row} - 1][n - \textit{col} - 1] = \textit{matrix}[\textit{col}][n - \textit{row} - 1]
$$
同样地,直接赋值会覆盖掉 $\textit{matrix}[n - \textit{row} - 1][n - \textit{col} - 1]$ 原来的值,因此我们还是需要使用一个临时变量进行存储,不过这次,我们可以直接使用之前的临时变量 $\textit{temp}$:
$$
\left{
\begin{alignedat}{2}
&\textit{temp} &&= \textit{matrix}[n - \textit{row} - 1][n - \textit{col} - 1]\
&\textit{matrix}[n - \textit{row} - 1][n - \textit{col} - 1] &&= \textit{matrix}[\textit{col}][n - \textit{row} - 1]\
&\textit{matrix}[\textit{col}][n - \textit{row} - 1] &&= \textit{matrix}[\textit{row}][\textit{col}]
\end{alignedat}
\right.
$$
我们再重复一次之前的操作,$\textit{matrix}[n - \textit{row} - 1][n - \textit{col} - 1]$ 经过旋转操作之后会到哪个位置呢?
$$
\left{
\begin{alignedat}{2}
& \textit{row} &&= n - \textit{row} - 1\
& \textit{col} &&= n - \textit{col} - 1
\end{alignedat}
\right.
$$
带入关键等式,就可以得到:
$$
\textit{matrix}[n - \textit{col} - 1][\textit{row}] = \textit{matrix}[n - \textit{row} - 1][n - \textit{col} - 1]
$$
写进去:
$$
\left{
\begin{alignedat}{2}
&\textit{temp} &&= \textit{matrix}[n - \textit{col} - 1][\textit{row}]\
&\textit{matrix}[n - \textit{col} - 1][\textit{row}] &&= \textit{matrix}[n - \textit{row} - 1][n - \textit{col} - 1]\
&\textit{matrix}[n - \textit{row} - 1][n - \textit{col} - 1] &&= \textit{matrix}[\textit{col}][n - \textit{row} - 1]\
&\textit{matrix}[\textit{col}][n - \textit{row} - 1] &&= \textit{matrix}[\textit{row}][\textit{col}]
\end{alignedat}
\right.
$$
不要灰心,再来一次!$\textit{matrix}[n - \textit{col} - 1][\textit{row}]$ 经过旋转操作之后回到哪个位置呢?
$$
\left{
\begin{alignedat}{2}
& \textit{row} &&= n - \textit{col} - 1\
& \textit{col} &&= \textit{row}
\end{alignedat}
\right.
$$
带入关键等式,就可以得到:
$$
\textit{matrix}[\textit{row}][\textit{col}] = \textit{matrix}[n - \textit{col} - 1][\textit{row}]
$$
我们回到了最初的起点 $\textit{matrix}[\textit{row}][\textit{col}]$,也就是说:
$$
\begin{cases}
\textit{matrix}[\textit{row}][\textit{col}]\
\textit{matrix}[\textit{col}][n - \textit{row} - 1]\
\textit{matrix}[n - \textit{row} - 1][n - \textit{col} - 1]\
\textit{matrix}[n - \textit{col} - 1][\textit{row}]
\end{cases}
$$
这四项处于一个循环中,并且每一项旋转后的位置就是下一项所在的位置!因此我们可以使用一个临时变量 $\textit{temp}$ 完成这四项的原地交换:
$$
\left{
\begin{alignedat}{2}
&\textit{temp} &&= \textit{matrix}[\textit{row}][\textit{col}]\
&\textit{matrix}[\textit{row}][\textit{col}] &&= \textit{matrix}[n - \textit{col} - 1][\textit{row}]\
&\textit{matrix}[n - \textit{col} - 1][\textit{row}] &&= \textit{matrix}[n - \textit{row} - 1][n - \textit{col} - 1]\
&\textit{matrix}[n - \textit{row} - 1][n - \textit{col} - 1] &&= \textit{matrix}[\textit{col}][n - \textit{row} - 1]\
&\textit{matrix}[\textit{col}][n - \textit{row} - 1] &&= \textit{temp}
\end{alignedat}
\right.
$$
当我们知道了如何原地旋转矩阵之后,还有一个重要的问题在于:我们应该枚举哪些位置 $(\textit{row}, \textit{col})$ 进行上述的原地交换操作呢?由于每一次原地交换四个位置,因此:
{:width="80%"}
保证了不重复、不遗漏;
{:width="80%"}
同样保证了不重复、不遗漏,矩阵正中央的点无需旋转。
###C++
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for (int i = 0; i < n / 2; ++i) {
for (int j = 0; j < (n + 1) / 2; ++j) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n - j - 1][i];
matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
matrix[j][n - i - 1] = temp;
}
}
}
};
###C++
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for (int i = 0; i < n / 2; ++i) {
for (int j = 0; j < (n + 1) / 2; ++j) {
tie(matrix[i][j], matrix[n - j - 1][i], matrix[n - i - 1][n - j - 1], matrix[j][n - i - 1]) \
= make_tuple(matrix[n - j - 1][i], matrix[n - i - 1][n - j - 1], matrix[j][n - i - 1], matrix[i][j]);
}
}
}
};
###Java
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
for (int i = 0; i < n / 2; ++i) {
for (int j = 0; j < (n + 1) / 2; ++j) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n - j - 1][i];
matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
matrix[j][n - i - 1] = temp;
}
}
}
}
###Python
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
for i in range(n // 2):
for j in range((n + 1) // 2):
matrix[i][j], matrix[n - j - 1][i], matrix[n - i - 1][n - j - 1], matrix[j][n - i - 1] \
= matrix[n - j - 1][i], matrix[n - i - 1][n - j - 1], matrix[j][n - i - 1], matrix[i][j]
###JavaScript
var rotate = function(matrix) {
const n = matrix.length;
for (let i = 0; i < Math.floor(n / 2); ++i) {
for (let j = 0; j < Math.floor((n + 1) / 2); ++j) {
const temp = matrix[i][j];
matrix[i][j] = matrix[n - j - 1][i];
matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
matrix[j][n - i - 1] = temp;
}
}
};
###Go
func rotate(matrix [][]int) {
n := len(matrix)
for i := 0; i < n/2; i++ {
for j := 0; j < (n+1)/2; j++ {
matrix[i][j], matrix[n-j-1][i], matrix[n-i-1][n-j-1], matrix[j][n-i-1] =
matrix[n-j-1][i], matrix[n-i-1][n-j-1], matrix[j][n-i-1], matrix[i][j]
}
}
}
###C
void rotate(int** matrix, int matrixSize, int* matrixColSize) {
for (int i = 0; i < matrixSize / 2; ++i) {
for (int j = 0; j < (matrixSize + 1) / 2; ++j) {
int temp = matrix[i][j];
matrix[i][j] = matrix[matrixSize - j - 1][i];
matrix[matrixSize - j - 1][i] = matrix[matrixSize - i - 1][matrixSize - j - 1];
matrix[matrixSize - i - 1][matrixSize - j - 1] = matrix[j][matrixSize - i - 1];
matrix[j][matrixSize - i - 1] = temp;
}
}
}
复杂度分析
时间复杂度:$O(N^2)$,其中 $N$ 是 $\textit{matrix}$ 的边长。我们需要枚举的子矩阵大小为 O($\lfloor n/2 \rfloor \times \lfloor (n+1)/2 \rfloor) = O(N^2)$。
空间复杂度:$O(1)$。为原地旋转。
我们还可以另辟蹊径,用翻转操作代替旋转操作。我们还是以题目中的示例二
$$
\begin{bmatrix}
5 & 1 & 9 & 11 \
2 & 4 & 8 & 10 \
13 & 3 & 6 & 7 \
15 & 14 & 12 & 16
\end{bmatrix}
$$
作为例子,先将其通过水平轴翻转得到:
$$
\begin{bmatrix}
5 & 1 & 9 & 11 \
2 & 4 & 8 & 10 \
13 & 3 & 6 & 7 \
15 & 14 & 12 & 16
\end{bmatrix}
\xRightarrow[]{水平翻转}
\begin{bmatrix}
15 & 14 & 12 & 16 \
13 & 3 & 6 & 7 \
2 & 4 & 8 & 10 \
5 & 1 & 9 & 11
\end{bmatrix}
$$
再根据主对角线翻转得到:
$$
\begin{bmatrix}
15 & 14 & 12 & 16 \
13 & 3 & 6 & 7 \
2 & 4 & 8 & 10 \
5 & 1 & 9 & 11
\end{bmatrix}
\xRightarrow[]{主对角线翻转}
\begin{bmatrix}
15 & 13 & 2 & 5 \
14 & 3 & 4 & 1 \
12 & 6 & 8 & 9 \
16 & 7 & 10 & 11
\end{bmatrix}
$$
就得到了答案。这是为什么呢?对于水平轴翻转而言,我们只需要枚举矩阵上半部分的元素,和下半部分的元素进行交换,即
$$
\textit{matrix}[\textit{row}][\textit{col}] \xRightarrow[]{水平轴翻转} \textit{matrix}[n - \textit{row} - 1][\textit{col}]
$$
对于主对角线翻转而言,我们只需要枚举对角线左侧的元素,和右侧的元素进行交换,即
$$
\textit{matrix}[\textit{row}][\textit{col}] \xRightarrow[]{主对角线翻转} \textit{matrix}[\textit{col}][\textit{row}]
$$
将它们联立即可得到:
$$
\begin{aligned}
\textit{matrix}[\textit{row}][\textit{col}] & \xRightarrow[]{水平轴翻转} \textit{matrix}[n - \textit{row} - 1][\textit{col}] \
&\xRightarrow[]{主对角线翻转} \textit{matrix}[\textit{col}][n - \textit{row} - 1]
\end{aligned}
$$
和方法一、方法二中的关键等式:
$$
\textit{matrix}_\textit{new}[\textit{col}][n - \textit{row} - 1] = \textit{matrix}[\textit{row}][\textit{col}]
$$
是一致的。
###C++
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 水平翻转
for (int i = 0; i < n / 2; ++i) {
for (int j = 0; j < n; ++j) {
swap(matrix[i][j], matrix[n - i - 1][j]);
}
}
// 主对角线翻转
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
swap(matrix[i][j], matrix[j][i]);
}
}
}
};
###Java
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 水平翻转
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 - i - 1][j];
matrix[n - i - 1][j] = temp;
}
}
// 主对角线翻转
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
}
}
###Python
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
# 水平翻转
for i in range(n // 2):
for j in range(n):
matrix[i][j], matrix[n - i - 1][j] = matrix[n - i - 1][j], matrix[i][j]
# 主对角线翻转
for i in range(n):
for j in range(i):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
###JavaScript
var rotate = function(matrix) {
const n = matrix.length;
// 水平翻转
for (let i = 0; i < Math.floor(n / 2); i++) {
for (let j = 0; j < n; j++) {
[matrix[i][j], matrix[n - i - 1][j]] = [matrix[n - i - 1][j], matrix[i][j]];
}
}
// 主对角线翻转
for (let i = 0; i < n; i++) {
for (let j = 0; j < i; j++) {
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
}
}
};
###Go
func rotate(matrix [][]int) {
n := len(matrix)
// 水平翻转
for i := 0; i < n/2; i++ {
matrix[i], matrix[n-1-i] = matrix[n-1-i], matrix[i]
}
// 主对角线翻转
for i := 0; i < n; i++ {
for j := 0; j < i; j++ {
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
}
}
}
###C
void swap(int* a, int* b) {
int t = *a;
*a = *b, *b = t;
}
void rotate(int** matrix, int matrixSize, int* matrixColSize) {
// 水平翻转
for (int i = 0; i < matrixSize / 2; ++i) {
for (int j = 0; j < matrixSize; ++j) {
swap(&matrix[i][j], &matrix[matrixSize - i - 1][j]);
}
}
// 主对角线翻转
for (int i = 0; i < matrixSize; ++i) {
for (int j = 0; j < i; ++j) {
swap(&matrix[i][j], &matrix[j][i]);
}
}
}
复杂度分析
时间复杂度:$O(N^2)$,其中 $N$ 是 $\textit{matrix}$ 的边长。对于每一次翻转操作,我们都需要枚举矩阵中一半的元素。
空间复杂度:$O(1)$。为原地翻转得到的原地旋转。
给定两个字符串, s 和 goal。如果在若干次旋转操作之后,s 能变成 goal ,那么返回 true 。
s 的 旋转操作 就是将 s 最左边的字符移动到最右边。
s = 'abcde',在旋转一次之后结果就是'bcdea' 。
示例 1:
输入: s = "abcde", goal = "cdeab" 输出: true
示例 2:
输入: s = "abcde", goal = "abced" 输出: false
提示:
1 <= s.length, goal.length <= 100s 和 goal 由小写英文字母组成例如 $s=\texttt{abcde}$,旋转一次变成 $\texttt{bcdea}$,再旋转一次变成 $\texttt{cdeab}$。旋转后的字符串是 $s$ 的后缀加上 $s$ 的前缀,例如 $\texttt{cdeab} = \texttt{cde} + \texttt{ab}$。于是构造 $s+s = \texttt{abcdeabcde}$,这个字符串的任意长为 $n$ 的子串,都是 $s$ 的后缀加上 $s$ 的前缀。所以无论 $s$ 如何旋转,旋转后的字符串一定是 $s+s$ 的子串。
于是问题等价于:
注意题目没有保证 $s$ 和 $\textit{goal}$ 长度相等,如果不等,直接返回 $\texttt{false}$。
class Solution:
def rotateString(self, s: str, goal: str) -> bool:
return len(s) == len(goal) and goal in s + s
class Solution {
public boolean rotateString(String s, String goal) {
return s.length() == goal.length() && (s + s).contains(goal);
}
}
class Solution {
public:
bool rotateString(string s, string goal) {
return s.size() == goal.size() && (s + s).contains(goal);
}
};
func rotateString(s, goal string) bool {
return len(s) == len(goal) && strings.Contains(s+s, goal)
}
用 KMP、Z 函数、字符串哈希等算法,都可以 $\mathcal{O}(n)$ 判断 $s+s$ 是否包含 $\textit{goal}$。
下面用的 KMP 算法,原理见 KMP 算法讲解。
# 下面是 KMP 模板。对于本题来说,找到一个就可以返回了。为方便大家使用,我保留了完整的模板。
# 在文本串 text 中查找模式串 pattern,返回所有成功匹配的位置(pattern[0] 在 text 中的下标)
def kmp_search(text: str, pattern: str) -> List[int]:
m = len(pattern)
pi = [0] * m
cnt = 0
for i in range(1, m):
b = pattern[i]
while cnt and pattern[cnt] != b:
cnt = pi[cnt - 1]
if pattern[cnt] == b:
cnt += 1
pi[i] = cnt
pos = []
cnt = 0
for i, b in enumerate(text):
while cnt and pattern[cnt] != b:
cnt = pi[cnt - 1]
if pattern[cnt] == b:
cnt += 1
if cnt == len(pattern):
pos.append(i - m + 1)
cnt = pi[cnt - 1]
return pos
class Solution:
def rotateString(self, s: str, goal: str) -> bool:
return len(s) == len(goal) and len(kmp_search(s + s, goal)) > 0
class Solution {
public boolean rotateString(String s, String goal) {
return s.length() == goal.length() &&
!kmpSearch((s + s).toCharArray(), goal.toCharArray()).isEmpty();
}
// 下面是 KMP 模板。对于本题来说,找到一个就可以返回了。为方便大家使用,我保留了完整的模板。
// 在文本串 text 中查找模式串 pattern,返回所有成功匹配的位置(pattern[0] 在 text 中的下标)
private List<Integer> kmpSearch(char[] text, char[] pattern) {
int m = pattern.length;
int[] pi = new int[m];
int cnt = 0;
for (int i = 1; i < m; i++) {
char b = pattern[i];
while (cnt > 0 && pattern[cnt] != b) {
cnt = pi[cnt - 1];
}
if (pattern[cnt] == b) {
cnt++;
}
pi[i] = cnt;
}
List<Integer> pos = new ArrayList<>();
cnt = 0;
for (int i = 0; i < text.length; i++) {
char b = text[i];
while (cnt > 0 && pattern[cnt] != b) {
cnt = pi[cnt - 1];
}
if (pattern[cnt] == b) {
cnt++;
}
if (cnt == m) {
pos.add(i - m + 1);
cnt = pi[cnt - 1];
}
}
return pos;
}
}
class Solution {
// 下面是 KMP 模板。对于本题来说,找到一个就可以返回了。为方便大家使用,我保留了完整的模板。
// 在文本串 text 中查找模式串 pattern,返回所有成功匹配的位置(pattern[0] 在 text 中的下标)
vector<int> kmp_search(const string& text, const string& pattern) {
int m = pattern.size();
vector<int> pi(m);
int cnt = 0;
for (int i = 1; i < m; i++) {
char b = pattern[i];
while (cnt && pattern[cnt] != b) {
cnt = pi[cnt - 1];
}
if (pattern[cnt] == b) {
cnt++;
}
pi[i] = cnt;
}
vector<int> pos;
cnt = 0;
for (int i = 0; i < text.size(); i++) {
char b = text[i];
while (cnt && pattern[cnt] != b) {
cnt = pi[cnt - 1];
}
if (pattern[cnt] == b) {
cnt++;
}
if (cnt == m) {
pos.push_back(i - m + 1);
cnt = pi[cnt - 1];
}
}
return pos;
}
public:
bool rotateString(string s, string goal) {
return s.size() == goal.size() && !kmp_search(s + s, goal).empty();
}
};
// 下面是 KMP 模板。对于本题来说,找到一个就可以返回了。为方便大家使用,我保留了完整的模板。
// 在文本串 text 中查找模式串 pattern,返回所有成功匹配的位置(pattern[0] 在 text 中的下标)
func kmpSearch(text, pattern string) (pos []int) {
m := len(pattern)
pi := make([]int, m)
cnt := 0
for i := 1; i < m; i++ {
b := pattern[i]
for cnt > 0 && pattern[cnt] != b {
cnt = pi[cnt-1]
}
if pattern[cnt] == b {
cnt++
}
pi[i] = cnt
}
cnt = 0
for i, b := range text {
for cnt > 0 && pattern[cnt] != byte(b) {
cnt = pi[cnt-1]
}
if pattern[cnt] == byte(b) {
cnt++
}
if cnt == m {
pos = append(pos, i-m+1)
cnt = pi[cnt-1]
}
}
return
}
func rotateString(s, goal string) bool {
return len(s) == len(goal) && kmpSearch(s+s, goal) != nil
}
见下面字符串题单的「一、KMP」。
欢迎关注 B站@灵茶山艾府
由于每次旋转操作都是将最左侧字符移动到最右侧,因此如果 goal 可由 s 经过多步旋转而来,那么 goal 必然会出现在 s + s 中,即满足 (s + s).contains(goal),同时为了 s 本身过长导致的结果成立,我们需要先确保两字符串长度相等。
代码:
###Java
class Solution {
public boolean rotateString(String s, String goal) {
return s.length() == goal.length() && (s + s).contains(goal);
}
}
contains 操作的复杂度说明看到不少同学对 contains 的复杂度写成 $O(n)$ 有疑问。
在 Java 中,contains 实际最终是通过 indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex) 来拿到子串在原串的下标,通过判断下标是否为 $-1$ 来得知子串是否在原串出现过。
我们知道一个较为普通的子串匹配算法的复杂度通为 $O(n*k)$,其中 $n$ 和 $k$ 分别是原串和子串的长度,而一些复杂度上较为优秀的算法可以做到 $O(n + k)$,例如 KMP。
从复杂度上来看 KMP 似乎要更好,但实际上对于 indexOf 这一高频操作而言,KMP 的预处理逻辑和空间开销都是不可接受的。
因此在 OpenJDK 中的 indexOf 源码中,你看不到诸如 KMP 这一类「最坏情况下仍为线性复杂度」的算法实现。
但是 contains 的复杂度真的就是 $O(n * k)$ 吗?
其实并不是,这取决于 JVM 是否有针对 indexOf 的优化,在最为流行 HotSpot VM 中,就有对 indexOf 的优化。
使用以下两行命令执行 Main.java,会得到不同的用时。
###Java
// Main.java
import java.util.*;
class Main {
static String ss = "1900216589537958049456207450268985232242852754963049829410964867980510717200606495004259179775210762723370289106970649635773837906542900276476226929871813370344374628795427969854262816333971458418647697497933767559786473164055741512717436542961770628985635269208255141092673831132865";
static String pp = "830411595466023844647269831101019568881117264597716557501027220546437084223034983361631430958163646150071031688420479928498493050624766427709034028819288384316713084883575266906600102801186671777455503932259958027055697399984336592981698127456301551509241";
static int cnt = (int) 1e8;
static public void main(String[] args) {
long start = System.currentTimeMillis();
while (cnt-- > 0) ss.contains(pp);
System.out.println(System.currentTimeMillis() - start);
}
}
环境说明:
###Shell
➜ java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
先执行 javac Main.java 进行编译后:
indexOf 实现进行匹配(执行多次,平均耗时为基准值 $X$):java -XX:+UnlockDiagnosticVMOptions -XX:DisableIntrinsic=_indexOf Main
indexOf 进行匹配(执行多次,平均耗时为基准值 $X$ 的 $[0.55, 0.65]$ 之间):java Main
因此实际运行的 contains 操作的复杂度为多少并不好确定,但可以确定是要优于 $O(n * k)$ 的。
今日份加餐 :【面试高频题】难度 2/5,结合二分的序列 DP 运用题 🍭🍭🍭🍭
或是考虑加练如下「模拟」题 🍭🍭🍭
| 题目 | 题解 | 难度 | 推荐指数 |
|---|---|---|---|
| 6. Z 字形变换 | LeetCode 题解链接 | 中等 | 🤩🤩🤩 |
| 8. 字符串转换整数 (atoi) | LeetCode 题解链接 | 中等 | 🤩🤩🤩 |
| 12. 整数转罗马数字 | LeetCode 题解链接 | 中等 | 🤩🤩 |
| 59. 螺旋矩阵 II | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 65. 有效数字 | LeetCode 题解链接 | 困难 | 🤩🤩🤩 |
| 73. 矩阵置零 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 89. 格雷编码 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 166. 分数到小数 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 260. 只出现一次的数字 III | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 414. 第三大的数 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 419. 甲板上的战舰 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 443. 压缩字符串 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 457. 环形数组是否存在循环 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 528. 按权重随机选择 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 539. 最小时间差 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 726. 原子的数量 | LeetCode 题解链接 | 困难 | 🤩🤩🤩🤩 |
注:以上目录整理来自 wiki,任何形式的转载引用请保留出处。
如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/
也欢迎你 关注我 和 加入我们的「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。
所有题解已经加入 刷题指南,欢迎 star 哦 ~
思路
首先,如果 $s$ 和 $\textit{goal}$ 的长度不一样,那么无论怎么旋转,$s$ 都不能得到 $\textit{goal}$,返回 $\text{false}$。在长度一样(都为 $n$)的前提下,假设 $s$ 旋转 $i$ 位,则与 $\textit{goal}$ 中的某一位字符 $\textit{goal}[j]$ 对应的原 $s$ 中的字符应该为 $s[(i+j) \bmod n]$。在固定 $i$ 的情况下,遍历所有 $j$,若对应字符都相同,则返回 $\text{true}$。否则,继续遍历其他候选的 $i$。若所有的 $i$ 都不能使 $s$ 变成 $\textit{goal}$,则返回 $\text{false}$。
代码
###Python
class Solution:
def rotateString(self, s: str, goal: str) -> bool:
m, n = len(s), len(goal)
if m != n:
return False
for i in range(n):
for j in range(n):
if s[(i + j) % n] != goal[j]:
break
else:
return True
return False
###Java
class Solution {
public boolean rotateString(String s, String goal) {
int m = s.length(), n = goal.length();
if (m != n) {
return false;
}
for (int i = 0; i < n; i++) {
boolean flag = true;
for (int j = 0; j < n; j++) {
if (s.charAt((i + j) % n) != goal.charAt(j)) {
flag = false;
break;
}
}
if (flag) {
return true;
}
}
return false;
}
}
###C#
public class Solution {
public bool rotateString(string s, string goal) {
int m = s.Length, n = goal.Length;
if (m != n) {
return false;
}
for (int i = 0; i < n; i++) {
bool flag = true;
for (int j = 0; j < n; j++) {
if (s[(i + j) % n] != goal[j]) {
flag = false;
break;
}
}
if (flag) {
return true;
}
}
return false;
}
}
###C++
class Solution {
public:
bool rotateString(string s, string goal) {
int m = s.size(), n = goal.size();
if (m != n) {
return false;
}
for (int i = 0; i < n; i++) {
bool flag = true;
for (int j = 0; j < n; j++) {
if (s[(i + j) % n] != goal[j]) {
flag = false;
break;
}
}
if (flag) {
return true;
}
}
return false;
}
};
###C
bool rotateString(char * s, char * goal){
int m = strlen(s), n = strlen(goal);
if (m != n) {
return false;
}
for (int i = 0; i < n; i++) {
bool flag = true;
for (int j = 0; j < n; j++) {
if (s[(i + j) % n] != goal[j]) {
flag = false;
break;
}
}
if (flag) {
return true;
}
}
return false;
}
###JavaScript
var rotateString = function(s, goal) {
const m = s.length, n = goal.length;
if (m !== n) {
return false;
}
for (let i = 0; i < n; i++) {
let flag = true;
for (let j = 0; j < n; j++) {
if (s[(i + j) % n] !== goal[j]) {
flag = false;
break;
}
}
if (flag) {
return true;
}
}
return false;
};
###go
func rotateString(s, goal string) bool {
n := len(s)
if n != len(goal) {
return false
}
next:
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
if s[(i+j)%n] != goal[j] {
continue next
}
}
return true
}
return false
}
复杂度分析
时间复杂度:$O(n^2)$,其中 $n$ 是字符串 $s$ 的长度。我们需要双重循环来判断。
空间复杂度:$O(1)$。仅使用常数空间。
思路
首先,如果 $s$ 和 $\textit{goal}$ 的长度不一样,那么无论怎么旋转,$s$ 都不能得到 $\textit{goal}$,返回 $\text{false}$。字符串 $s + s$ 包含了所有 $s$ 可以通过旋转操作得到的字符串,只需要检查 $\textit{goal}$ 是否为 $s + s$ 的子字符串即可。具体可以参考「28. 实现 strStr() 的官方题解」的实现代码,本题解中采用直接调用库函数的方法。
代码
###Python
class Solution:
def rotateString(self, s: str, goal: str) -> bool:
return len(s) == len(goal) and goal in s + s
###Java
class Solution {
public boolean rotateString(String s, String goal) {
return s.length() == goal.length() && (s + s).contains(goal);
}
}
###C#
public class Solution {
public bool rotateString(string s, string goal) {
return s.Length == goal.Length && (s + s).Contains(goal);
}
}
###C++
class Solution {
public:
bool rotateString(string s, string goal) {
return s.size() == goal.size() && (s + s).find(goal) != string::npos;
}
};
###C
bool rotateString(char * s, char * goal){
int m = strlen(s), n = strlen(goal);
if (m != n) {
return false;
}
char * str = (char *)malloc(sizeof(char) * (m + n + 1));
sprintf(str, "%s%s", goal, goal);
return strstr(str, s) != NULL;
}
###JavaScript
var rotateString = function(s, goal) {
return s.length === goal.length && (s + s).indexOf(goal) !== -1;
};
###go
func rotateString(s, goal string) bool {
return len(s) == len(goal) && strings.Contains(s+s, goal)
}
复杂度分析
时间复杂度:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。$\text{KMP}$ 算法搜索子字符串的时间复杂度为 $O(n)$,其他搜索子字符串的方法会略有差异。
空间复杂度:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。$\text{KMP}$ 算法搜索子字符串的空间复杂度为 $O(n)$,其他搜索子字符串的方法会略有差异。
我们称一个数 X 为好数, 如果它的每位数字逐个地被旋转 180 度后,我们仍可以得到一个有效的,且和 X 不同的数。要求每位数字都要被旋转。
如果一个数的每位数字被旋转以后仍然还是一个数字, 则这个数是有效的。0, 1, 和 8 被旋转后仍然是它们自己;2 和 5 可以互相旋转成对方(在这种情况下,它们以不同的方向旋转,换句话说,2 和 5 互为镜像);6 和 9 同理,除了这些以外其他的数字旋转以后都不再是有效的数字。
现在我们有一个正整数 N, 计算从 1 到 N 中有多少个数 X 是好数?
示例:
输入: 10 输出: 4 解释: 在[1, 10]中有四个好数: 2, 5, 6, 9。 注意 1 和 10 不是好数, 因为他们在旋转之后不变。
提示:
[1, 10000]。利用 $n$ 的范围为 $1e4$,我们可以直接检查 $[1, n]$ 的每个数。
由于每一个位数都需要翻转,因此如果当前枚举到的数值 x 中包含非有效翻转数字(非 0125689)则必然不是好数;而在每一位均为有效数字的前提下,若当前枚举到的数值 x 中包含翻转后能够发生数值上变化的数值(2569),则为好数。
代码:
###Java
class Solution {
public int rotatedDigits(int n) {
int ans = 0;
out:for (int i = 1; i <= n; i++) {
boolean ok = false;
int x = i;
while (x != 0) {
int t = x % 10;
x /= 10;
if (t == 2 || t == 5 || t == 6 || t == 9) ok = true;
else if (t != 0 && t != 1 && t != 8) continue out;
}
if (ok) ans++;
}
return ans;
}
}
###TypeScript
function rotatedDigits(n: number): number {
let ans = 0
out:for (let i = 1; i <= n; i++) {
let ok = false
let x = i
while (x != 0) {
const t = x % 10
x = Math.floor(x / 10)
if (t == 2 || t == 5 || t == 6 || t == 9) ok = true
else if (t != 0 && t != 1 && t != 8) continue out
}
if (ok) ans++
}
return ans
};
###Python3
class Solution:
def rotatedDigits(self, n: int) -> int:
ans = 0
for i in range(1, n + 1):
ok, x = False, i
while x != 0:
t = x % 10
x = x // 10
if t == 2 or t == 5 or t == 6 or t == 9:
ok = True
elif t != 0 and t != 1 and t != 8:
ok = False
break
ans = ans + 1 if ok else ans
return ans
如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/
也欢迎你 关注我 和 加入我们的「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。
所有题解已经加入 刷题指南,欢迎 star 哦 ~
视频讲解,从 19:30 开始(基于题目 2376. 统计特殊整数)。
讲了数位 DP 的通用模板,以及如何使用该模板秒杀相关困难题目。
讲完题目后还讲了一些上分的训练技巧。
根据题意,好数中不能有 $3,4,7$,且至少包含 $2,5,6,9$ 中的一个。
将 $n$ 转换成字符串 $s$,定义 $f(i,\textit{hasDiff}, \textit{isLimit}, \textit{isNum})$ 表示构造从左往右第 $i$ 位及其之后数位的合法方案数,其余参数的含义为:
后面两个参数可适用于其它数位 DP 题目。
枚举要填入的数字,具体实现逻辑见代码。对于本题来说,由于前导零对答案无影响,$\textit{isNum}$ 可以省略。
下面代码中 Java/C++/Go 只需要记忆化 $(i,\textit{hasDiff})$ 这个状态,因为:
!isLimit 成立时才去记忆化。接着上面的例子,在前面填 $12$ 的时候,下一位填的数字不能超过 $3$,因此算出来的结果是不能套用到前面填的是 $10,11$ 这些数字上面的。DIFFS = (0, 0, 1, -1, -1, 1, 1, -1, 0, 1)
class Solution:
def rotatedDigits(self, n: int) -> int:
s = str(n)
@cache
def f(i: int, has_diff: bool, is_limit: bool) -> int:
if i == len(s):
return has_diff # 只有包含 2/5/6/9 才算一个好数
res = 0
up = int(s[i]) if is_limit else 9
for d in range(0, up + 1): # 枚举要填入的数字 d
if DIFFS[d] != -1: # d 不是 3/4/7
res += f(i + 1, has_diff or DIFFS[d], is_limit and d == up)
return res
return f(0, False, True)
class Solution {
private static int[] DIFFS = {0, 0, 1, -1, -1, 1, 1, -1, 0, 1};
public int rotatedDigits(int n) {
char[] s = Integer.toString(n).toCharArray();
int[][] memo = new int[s.length][2];
for (int[] row : memo) {
Arrays.fill(row, -1);
}
return dfs(0, 0, true, s, memo);
}
private int dfs(int i, int hasDiff, boolean isLimit, char[] s, int[][] memo) {
if (i == s.length) {
return hasDiff; // 只有包含 2/5/6/9 才算一个好数
}
if (!isLimit && memo[i][hasDiff] >= 0) {
return memo[i][hasDiff];
}
int res = 0;
int up = isLimit ? s[i] - '0' : 9;
for (int d = 0; d <= up; d++) { // 枚举要填入的数字 d
if (DIFFS[d] != -1) { // d 不是 3/4/7
res += dfs(i + 1, hasDiff | DIFFS[d], isLimit && d == up, s, memo);
}
}
if (!isLimit) {
memo[i][hasDiff] = res;
}
return res;
}
}
int diffs[10] = {0, 0, 1, -1, -1, 1, 1, -1, 0, 1};
class Solution {
public:
int rotatedDigits(int n) {
string s = to_string(n);
int m = s.size();
vector<array<int, 2>> memo(m, {-1, -1});
auto dfs = [&](this auto&& dfs, int i, bool has_diff, bool is_limit) -> int {
if (i == m) {
return has_diff; // 只有包含 2/5/6/9 才算一个好数
}
if (!is_limit && memo[i][has_diff] >= 0) {
return memo[i][has_diff];
}
int res = 0;
int up = is_limit ? s[i] - '0' : 9;
for (int d = 0; d <= up; d++) { // 枚举要填入的数字 d
if (diffs[d] != -1) { // d 不是 3/4/7
res += dfs(i + 1, has_diff || diffs[d], is_limit && d == up);
}
}
if (!is_limit) {
memo[i][has_diff] = res;
}
return res;
};
return dfs(0, false, true);
}
};
var diffs = [10]int{0, 0, 1, -1, -1, 1, 1, -1, 0, 1}
func rotatedDigits(n int) int {
s := strconv.Itoa(n)
m := len(s)
memo := make([][2]int, m)
for i := range memo {
memo[i] = [2]int{-1, -1}
}
var dfs func(int, int, bool) int
dfs = func(i, isDiff int, isLimit bool) (res int) {
if i == m {
return isDiff // 只有包含 2/5/6/9 才算一个好数
}
if !isLimit {
p := &memo[i][isDiff]
if *p >= 0 {
return *p
}
defer func() { *p = res }()
}
up := 9
if isLimit {
up = int(s[i] - '0')
}
for d := 0; d <= up; d++ { // 枚举要填入的数字 d
if diffs[d] != -1 { // d 不是 3/4/7
res += dfs(i+1, isDiff|diffs[d], isLimit && d == up)
}
}
return
}
return dfs(0, 0, true)
}
见下面动态规划题单的「十、数位 DP」。
欢迎关注 B站@灵茶山艾府
动归数组d,d[i]表示数字i的状态。
d[i]对应有三个值,1是好数,0是普数,-1是坏数。
好数是旋转后不同的数比如(2,5,6,9),普数是旋转以后相同的数如(0,1,8),坏数是旋转后不能成立的数如(3,4,7),这里前十个数的好坏情况通过切片给预处理了。
某数字i末位或末位以外的数字为坏数,数字i就肯定是坏数,例如(897,389,941)等等。
如果数字i不是坏数,那只要末位或末位以外的数字存在好数,那数字i就肯定是好数,如(120,886,509)等等就是好数,如(808,880)就是普数,程序这里计算用了按位或"|"。
如果数字i的结果d[i]是1是好数,那总答案就加1。
主要是末位以外的数字在计算该数字前肯定计算过了,所以可以动归。
class Solution:
def rotatedDigits(self, N: int) -> int:
ans = 0
d = [0, 0, 1, -1, -1, 1, 1, -1, 0, 1] + [0] * (N - 9)
for i in range(N + 1):
if d[i // 10] == -1 or d[i % 10] == -1:
d[i] = -1
elif d[i // 10] == 1 or d[i % 10] == 1:
d[i] = 1
ans += 1
return ans
class Solution:
def rotatedDigits(self, N: int) -> int:
ans, d = 0, [0, 0, 1, -1, -1, 1, 1, -1, 0, 1] + [0] * (N - 9)
for i in range(N + 1):
d[i] = -1 in (d[i // 10], d[i % 10]) and -1 or d[i // 10] | d[i % 10]
ans += d[i] == 1
return ans
func rotatedDigits(N int) int {
ans, d := 0, make([]int, N + 1)
copy(d, []int{0, 0, 1, -1, -1, 1, 1, -1, 0, 1})
for i := 0; i <= N; i++ {
if d[i / 10] == -1 || d[i % 10] == -1 {
d[i] = -1
} else if d[i] = d[i / 10] | d[i % 10]; d[i] == 1 {
ans++
}
}
return ans
}
impl Solution {
pub fn rotated_digits(n: i32) -> i32 {
let mut d: Vec<i32> = vec![0, 0, 1, -1, -1, 1, 1, -1, 0, 1];
d.extend(vec![0; (n - 9).max(0) as usize]);
let mut ans = 0;
for i in 0..=n as usize {
d[i] = if d[i / 10] == -1 || d[i % 10] == -1 {-1} else {d[i / 10] | d[i % 10]};
ans += if d[i] == 1 {1} else {0};
}
ans
}
}
var rotatedDigits = function(N) {
let ans = 0
const d = [0, 0, 1, -1, -1, 1, 1, -1, 0, 1].concat(Array(Math.max(0, N - 9)).fill(0))
for (let i = 0; i <= N; ++i) {
if (d[Math.floor(i / 10)] == -1 || d[i % 10] == -1) {
d[i] = -1
} else if (d[Math.floor(i / 10)] == 1 || d[i % 10] == 1) {
d[i] = 1
++ans
}
}
return ans
};
function rotatedDigits(N: number): number {
let ans: number = 0;
let d: number[] = [0, 0, 1, -1, -1, 1, 1, -1, 0, 1, ...Array(Math.max(N - 9, 0)).fill(0)];
for (let i of Array.from({length: N + 1}, (_, k) => k)) {
let [j, k] = [Math.trunc(i / 10), i % 10];
d[i] = (d[j] == -1 || d[k] == -1) && -1 || d[j] | d[k];
ans += Number(d[i] == 1);
}
return ans
};
py
go
rs
js
ts
为方便描述,本文把 $\textit{nums}$ 简称为 $a$。
如果分别计算 $F(0),F(1),\ldots,F(n-1)$,总的时间复杂度是 $\mathcal{O}(n^2)$,太慢了。
考察相邻两项的差值,能否加快计算过程?
F(0) = (0 * 4) + (1 * 3) + (2 * 2) + (3 * 6)
F(1) = (0 * 6) + (1 * 4) + (2 * 3) + (3 * 2)
F(2) = (0 * 2) + (1 * 6) + (2 * 4) + (3 * 3)
F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4)
从 $F(0)$ 到 $F(1)$,$a_0,a_1,a_2,a_3$ 的系数是怎么变的?
所以 $F(1) = F(0) + a_0+a_1+a_2-3a_3 = F(0) + (a_0+a_1+a_2+a_3)-4a_3$。
这意味着,如果已知 $F(0)$ 以及 $a$ 的总和 $S$,就可以 $\mathcal{O}(1)$ 算出
$$
F(1) = F(0) + S - n\cdot a_{n-1}
$$
从 $F(1)$ 到 $F(2)$,$a_0,a_1,a_2,a_3$ 的系数是怎么变的?
所以有
$$
F(2) = F(1) + S - n\cdot a_{n-2}
$$
一般地,从 $F(i-1)$ 到 $F(i)$:
所以有
$$
F(i) = F(i-1) + S - n\cdot a_{n-i}
$$
根据上式,我们可以先算出 $F(0)$,然后从 $F(0)$ 开始,递推算出 $F(1),F(2),\ldots,F(n-1)$。
答案为所有 $F(i)$ 的最大值。
class Solution:
def maxRotateFunction(self, nums: list[int]) -> int:
n = len(nums)
f = sum(i * x for i, x in enumerate(nums)) # F(0)
s = sum(nums) # nums 的总和
ans = f
for i in range(n - 1, 0, -1):
f += s - n * nums[i]
ans = max(ans, f)
return ans
class Solution {
public int maxRotateFunction(int[] nums) {
int n = nums.length;
int f = 0;
int sum = 0;
for (int i = 0; i < n; i++) {
f += i * nums[i]; // 计算 F(0)
sum += nums[i]; // 计算 nums 的总和
}
int ans = f;
for (int i = n - 1; i > 0; i--) {
f += sum - n * nums[i];
ans = Math.max(ans, f);
}
return ans;
}
}
class Solution {
public:
int maxRotateFunction(vector<int>& nums) {
int n = nums.size();
int f = 0;
int sum = 0;
for (int i = 0; i < n; i++) {
f += i * nums[i]; // 计算 F(0)
sum += nums[i]; // 计算 nums 的总和
}
int ans = f;
for (int i = n - 1; i > 0; i--) {
f += sum - n * nums[i];
ans = max(ans, f);
}
return ans;
}
};
int maxRotateFunction(int* nums, int numsSize) {
int f = 0;
int sum = 0;
for (int i = 0; i < numsSize; i++) {
f += i * nums[i]; // 计算 F(0)
sum += nums[i]; // 计算 nums 的总和
}
int ans = f;
for (int i = numsSize - 1; i > 0; i--) {
f += sum - numsSize * nums[i];
ans = MAX(ans, f);
}
return ans;
}
func maxRotateFunction(nums []int) int {
n := len(nums)
f := 0
sum := 0
for i, x := range nums {
f += i * x // 计算 F(0)
sum += x // 计算 nums 的总和
}
ans := f
for i := n - 1; i > 0; i-- {
f += sum - n*nums[i]
ans = max(ans, f)
}
return ans
}
var maxRotateFunction = function(nums) {
const n = nums.length;
let f = 0;
let sum = 0;
for (let i = 0; i < n; i++) {
f += i * nums[i]; // 计算 F(0)
sum += nums[i]; // 计算 nums 的总和
}
let ans = f;
for (let i = n - 1; i > 0; i--) {
f += sum - n * nums[i];
ans = Math.max(ans, f);
}
return ans;
};
impl Solution {
pub fn max_rotate_function(nums: Vec<i32>) -> i32 {
let n = nums.len();
let mut f = 0;
let mut sum = 0;
for (i, &x) in nums.iter().enumerate() {
f += i as i32 * x; // 计算 F(0)
sum += x; // 计算 nums 的总和
}
let mut ans = f;
for i in (1..n).rev() {
f += sum - n as i32 * nums[i];
ans = ans.max(f);
}
ans
}
}
欢迎关注 B站@灵茶山艾府
给定一个长度为 n 的整数数组 nums 。
假设 arrk 是数组 nums 顺时针旋转 k 个位置后的数组,我们定义 nums 的 旋转函数 F 为:
F(k) = 0 * arrk[0] + 1 * arrk[1] + ... + (n - 1) * arrk[n - 1]返回 F(0), F(1), ..., F(n-1)中的最大值 。
生成的测试用例让答案符合 32 位 整数。
示例 1:
输入: nums = [4,3,2,6] 输出: 26 解释: F(0) = (0 * 4) + (1 * 3) + (2 * 2) + (3 * 6) = 0 + 3 + 4 + 18 = 25 F(1) = (0 * 6) + (1 * 4) + (2 * 3) + (3 * 2) = 0 + 4 + 6 + 6 = 16 F(2) = (0 * 2) + (1 * 6) + (2 * 4) + (3 * 3) = 0 + 6 + 8 + 9 = 23 F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4) = 0 + 2 + 12 + 12 = 26 所以 F(0), F(1), F(2), F(3) 中的最大值是 F(3) = 26 。
示例 2:
输入: nums = [100] 输出: 0
提示:
n == nums.length1 <= n <= 105-100 <= nums[i] <= 100为了方便,我们将 $nums$ 的长度记为 $n$。
题目要对「旋转数组」做逻辑,容易想到将 $nums$ 进行复制拼接,得到长度为 $2 * n$ 的新数组,在新数组上任意一个长度为 $n$ 的滑动窗口都对应了一个旋转数组。
然后考虑在窗口的滑动过程中,计算结果会如何变化,假设当前我们处理到下标为 $[i, i + n - 1]$ 的滑动窗口,根据题意,当前结果为:
$$
cur = nums[i] * 0 + nums[i + 1] * 1 + ... + nums[i + n - 1] * (n - 1)
$$
当窗口往后移动一位,也就是窗口的右端点来到 $i + n$ 的位置,左端点来到 $i + 1$ 的位置。
我们需要增加「新右端点」的值,即增加 $nums[i + n] * (n - 1)$,同时减去「旧左端点」的值,即减少 $nums[i] * 0$(固定为 $0$),然后更新新旧窗口的公共部分 $[i + 1, i + n - 1]$。
不难发现,随着窗口的逐步右移,每一位公共部分的权值系数都会进行减一。
$$
nums[i + 1] * 1 + nums[i + 2] * 2 + ... + nums[i + n - 1] * (n - 1)
$$
变为
$$
nums[i + 1] * 0 + nums[i + 2] * 1 + ... + nums[i + n - 1] * (n - 2)
$$
因此,公共部分的差值为 $\sum_{idx = i + 1}^{i + n - 1}nums[idx]$,这引导我们可以使用前缀和进行优化。
至此,我们从旧窗口到新窗口的过渡,都是 $O(1)$,整体复杂度为 $O(n)$。
实现上,我们并不需要真正对 $nums$ 进行复制拼接,而只需要在计算前缀和数组 $sum$ 进行简单的下标处理即可。
代码:
###Java
class Solution {
public int maxRotateFunction(int[] nums) {
int n = nums.length;
int[] sum = new int[n * 2 + 10];
for (int i = 1; i <= 2 * n; i++) sum[i] = sum[i - 1] + nums[(i - 1) % n];
int ans = 0;
for (int i = 1; i <= n; i++) ans += nums[i - 1] * (i - 1);
for (int i = n + 1, cur = ans; i < 2 * n; i++) {
cur += nums[(i - 1) % n] * (n - 1);
cur -= sum[i - 1] - sum[i - n];
if (cur > ans) ans = cur;
}
return ans;
}
}
如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/
也欢迎你 关注我 和 加入我们的「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。
所有题解已经加入 刷题指南,欢迎 star 哦 ~