二维数组的花式遍历技巧
在算法和数据结构中,二维数组(或矩阵)的遍历经常出现在各种题目中,尤其是涉及图像处理、矩阵变换、螺旋排序等场景。本文将基于几个经典的题目,分析二维数组的遍历技巧,并提供详细的解题思路和代码实现。
151. 反转字符串中的单词
给你一个字符串 s
,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s
中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
**注意:**输入字符串 s
中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
示例 1:
输入:s = "the sky is blue"
输出:"blue is sky the"
示例 2:
输入:s = " hello world "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。
示例 3:
输入:s = "a good example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。
解题思路:
- 去除多余空格:首先需要去掉字符串中的前导空格、尾随空格和多余的空格,确保单词之间只有一个空格。
- 反转单词顺序:我们可以将字符串按空格分割为单词,反转单词的顺序,再将它们按单个空格连接起来。
代码实现:
cpp
class Solution {
public:
string reverseWords(string s) {
string s1;
string s2;
//从后往前找相当于把整个字符串取反一次
for (int i=s.size()-1; i>=0; i--){
if (s[i]==' '){
if (!s1.empty()){
reverse(s1.begin(),s1.end());
if(!s2.empty()) {s2+=' ';}
s2 += s1;
s1.clear();
}
}
else{
s1 +=s[i];
}
}
// 处理最后一个单词
if (!s1.empty()) {
reverse(s1.begin(), s1.end());
if(!s2.empty()) {s2+=' ';}
s2 += s1;
}
return s2;
}
};
关键点:
- 反转单词 :使用
reverse()
对每个单词进行反转。 - 避免多余空格 :通过检查是否有非空的
s1
,确保不会将多余的空格添加到结果字符串中。
61. 旋转链表
给你一个链表的头节点 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]
解题思路:
- 计算链表长度:遍历链表,统计节点个数。
- 计算新的头节点 :将
k
对链表长度取余,得到实际需要旋转的步数。 - 旋转链表:找到旋转后的新的头节点,将链表的尾节点与头节点连接起来,然后切断原有的链接。
代码实现:
cpp
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if(k==0||head==nullptr) return head;
ListNode* p=head;
int count=0;
while(p!=nullptr){
count++;
p=p->next;
}
int num = count-(k % count);
ListNode* q=head;
ListNode* l=head;
for(int i=0;i<num-1;i++){
q=q->next;
}
for(int j=0;j<count-1;j++){
l=l->next;
}
l->next=head;
ListNode* newhead=q->next;
q->next=nullptr;
return newhead;
}
};
关键点:
- 链表长度:在旋转之前,先计算链表的长度,避免不必要的旋转。
- 链表断开:旋转后需要切断原链表的连接,将新的头节点与原链表的尾部连接。
48. 旋转图像
给定一个 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]]
解题思路:
- 矩阵转置 :首先对矩阵进行转置,矩阵的转置是将
matrix[i][j]
和matrix[j][i]
交换。 - 水平翻转:转置完成后,每一行进行反转即可实现顺时针旋转。
代码实现:
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; j<n; j++){//只对称调换上半部分
int temp = matrix[i][j];
matrix[i][j]=matrix[j][i];
matrix[j][i]=temp;
}
}
for (int i = 0; i < n; i++) {
reverse(matrix[i].begin(), matrix[i].end());
}
}
};
关键点:
- 原地操作:通过转置矩阵和反转每一行,实现了原地旋转。
- 空间复杂度:不使用额外空间,避免了不必要的空间浪费。
59. 螺旋矩阵 II
给你一个正整数 n
,生成一个包含 1
到 n2
所有元素,且元素按顺时针顺序螺旋排列的 n x n
正方形矩阵 matrix
。
示例 1:
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]
示例 2:
输入:n = 1
输出:[[1]]
解题思路:
- 四个边界 :设置
top
、bottom
、left
和right
四个变量来表示矩阵的边界。 - 填充数字:从左到右、从上到下、从右到左、从下到上依次填充数字。
- 调整边界:每次填充完一个方向后,调整对应的边界,直到所有位置都被填充完。
代码实现:
cpp
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
int top=0 , bottom=n-1, left=0, right= n-1;
int count=1;
vector<vector<int>> result(n,vector<int>(n,0));//这个一定要初始化,不然报错
while(top<=bottom && left<=right){
if(top<=bottom){
for(int i=left; i<=right; i++){
result[top][i]=count++;
}
top++;
}
if(left<=right){
for(int j=top; j<=bottom; j++){
result[j][right]=count++;
}
right--;
}
if(top<=bottom){
for(int i=right; i>=left; i--){
result[bottom][i]=count++;
}
bottom--;
}
if(left<=right){
for(int j=bottom; j>=top; j--){
result[j][left]=count++;
}
left++;
}
}
return result;
}
};
关键点:
- 螺旋填充:通过控制四个边界,逐步填充矩阵。
- 边界更新:每填充完一轮之后,更新边界,确保填充的范围逐渐缩小。
总结
在二维数组的遍历过程中,常见的技巧包括:
- 通过四个边界控制遍历范围:例如,螺旋矩阵和旋转矩阵的处理都依赖于逐步缩小的四个边界。
- 转置和反转:旋转矩阵问题通过先进行转置,然后再对行进行反转实现顺时针旋转。
- 分割与拼接:反转字符串中的单词的题目通过分割、反转和拼接来实现对字符串的转换。
这些技巧不仅提高了代码的效率,也有助于我们在面对类似问题时能够迅速找到解决方法。