1030. 距离顺序排列矩阵单元格●
描述
给定四个整数 row , cols , rCenter 和 cCenter 。
有一个 rows X cols 的矩阵,你在单元格上的坐标是 (rCenter, cCenter) 。
返回矩阵中的所有单元格的坐标,并按与 (rCenter, cCenter) 的 距离 从小到大排序。你可以按 任何 满足此条件的顺序返回答案。
单元格(r1, c1) 和 (r2, c2) 之间的距离为 |r1 - r2| + |c1 - c2|
。
示例
输入:rows = 2, cols = 2, rCenter = 0, cCenter = 1
输出:[[0,1],[0,0],[1,1],[1,0]]
解释:从 (r0, c0) 到其他单元格的距离为:[0,1,1,2]
[[0,1],[1,1],[0,0],[1,0]] 也会被视作正确答案。
题解
1. 直接排序
首先存储矩阵内所有的点,然后将其按照哈曼顿距离直接排序。
-
时间复杂度: O ( rows × cols × log ( rows × cols ) ) O(\textit{rows} \times \textit{cols} \times \log(\textit{rows} \times \textit{cols})) O(rows×cols×log(rows×cols)),存储所有点时间复杂度 O ( rows × cols ) O(\textit{rows} \times \textit{cols}) O(rows×cols),排序时间复杂度 O ( rows × cols log ( rows × cols ) ) O(\textit{rows} \times \textit{cols} \log(\textit{rows} \times \textit{cols})) O(rows×colslog(rows×cols))。
-
空间复杂度: O ( log ( rows × cols ) ) O(\log(\textit{rows} \times \textit{cols})) O(log(rows×cols)),即为排序需要使用的栈空间,不考虑返回值的空间占用。
class Solution {
public:
vector<vector<int>> allCellsDistOrder(int rows, int cols, int rCenter, int cCenter) {
vector<vector<int>> vecs(rows*cols, vector<int>(2, 0));
for(int i = 0; i < rows; ++i){
for(int j = 0; j < cols; ++j){
vecs[i*cols + j] = vector<int>{i, j};
}
}
sort(vecs.begin(), vecs.end(), [=](vector<int>& a, vector<int>& b) {
return abs(a[0] - rCenter) + abs(a[1] - cCenter) < abs(b[0] - rCenter) + abs(b[1] - cCenter);
});
return vecs;
}
};
2. 桶排序
方法一中排序的时间复杂度太高。实际在枚举所有点时,我们可以直接按照哈曼顿距离分桶。这样我们就可以实现线性的桶排序。
- 遍历所有坐标,按照距离的大小分组,每组的距离相等(即放入一个桶中)
- 按照距离从小到大的原则,遍历所有桶,并输出结果
本解法关键在于求得可能的最大距离,即行距离和列距离都最大时:max(r0, R - 1 - r0) + max(c0, C - 1 - c0)
- 时间复杂度: O ( rows × cols ) O(\textit{rows} \times \textit{cols}) O(rows×cols),存储所有点时间复杂度 O ( rows × cols ) O(\textit{rows} \times \textit{cols}) O(rows×cols),桶排序时间复杂度 O ( rows × cols ) O(\textit{rows} \times \textit{cols}) O(rows×cols)。
- 空间复杂度: O ( rows × cols ) O(\textit{rows} \times \textit{cols}) O(rows×cols),需要存储矩阵内所有点。
此解法时间复杂度为 O ( R ∗ C ) O(R*C) O(R∗C),理论上已达到最快可能,实际时间消耗会比预估要差,不同语言便利程度和优化不一,原因如下:
- 桶的制作涉及大量容器的初始化和存取
- 桶中要存储大量的坐标信息,不论是直接使用长度为 2 的小数组存储,还是用新的简单数据类,都会耗费很多时间
class Solution {
public:
vector<vector<int>> allCellsDistOrder(int rows, int cols, int rCenter, int cCenter) {
int maxDist = max(cCenter - 0, cols-1 - cCenter) + max(rCenter - 0, rows-1 - rCenter);
vector<vector<vector<int>>> bucket(maxDist + 1); // 桶
vector<vector<int>> ans(rows*cols);
for(int i = 0; i < rows; ++i){
for(int j = 0; j < cols; ++j){
int dist = abs(i - rCenter) + abs(j - cCenter);
bucket[dist].emplace_back(vector<int>{i ,j}); // 每个距离都分一个桶
}
}
int cnt = 0;
for(int i = 0; i <= maxDist; ++i){
for(auto& pos : bucket[i]){
ans[cnt] = pos; // 合并桶数据
++cnt;
}
}
return ans;
}
};
3. 几何法
曼哈顿距离相同的位置恰好构成一个斜着的正方形边框,因此我们可以从小到大枚举曼哈顿距离,并使用循环来直接枚举该距离对应的边框。
我们每次从该正方形边框的上顶点出发,依次经过右顶点、下顶点和左顶点,最后回到上顶点。这样即可完成当前层的遍历。
注意正方形边框中的部分点不一定落在矩阵中,所以我们需要做好边界判断。
-
顶点的判断方法有两组,分别对应和 r0 或 c0 是否相等
-
时间复杂度: O ( ( rows + cols ) 2 ) O\big((\textit{rows}+\textit{cols})^2\big) O((rows+cols)2),我们需要遍历矩阵内所有点,同时也会遍历部分超过矩阵部分的点。在最坏情况下,给定的单元格位于矩阵的一个角,例如 (0,0),此时最大的曼哈顿距离为 rows + cols − 2 \textit{rows}+\textit{cols}-2 rows+cols−2,需要遍历的点数为 2 ( rows + cols − 2 ) ( rows + cols − 1 ) + 1 2(\textit{rows}+\textit{cols}-2)(\textit{rows}+\textit{cols}-1)+1 2(rows+cols−2)(rows+cols−1)+1,因此时间复杂度为 O ( ( rows + cols ) 2 ) O\big((\textit{rows}+\textit{cols})^2\big) O((rows+cols)2)。
-
空间复杂度: O ( 1 ) O(1) O(1),不考虑返回值的空间占用。
class Solution {
public:
vector<vector<int>> allCellsDistOrder(int rows, int cols, int rCenter, int cCenter) {
const int dr[4] = {1, 1, -1, -1}; // ↘ ↙ ↖ ↗ 四个方向
const int dc[4] = {1, -1, -1, 1};
int row = rCenter, col = cCenter;
vector<vector<int>> ans(rows*cols, vector<int>(2, 0));
ans[0][0] = row;
ans[0][1] = col;
int cnt = 1;
int n = rows*cols;
while(cnt < n){
--row; // 从上顶点开始
for(int i = 0; i < 4; ++i){ // 同一个i,即为相同的前进方向(分别先后到达上、右、下、左、上顶点)
// ↘ i = 0 && row = rCenter 右顶点; ↙ i = 1 && col = cCenter 下顶点;
// ↖ i = 2 && row = rCenter 左顶点; ↗ i = 3 && col = cCenter 上顶点.
while((i % 2 == 0 && row != rCenter) || (i % 2 != 0 && col != cCenter)){
if(row >= 0 && row < rows && col >= 0 && col < cols){ // 排除方格外的点
ans[cnt][0] = row;
ans[cnt][1] = col;
++cnt;
}
row += dr[i]; // 按照当前方向移动
col += dc[i];
}
}
}
return ans;
}
};
- 除了通过顶点判断来切换移动方向外,还能通过当前的曼哈顿距离来得到每个方向的前进次数就是距离dist;
class Solution {
public:
vector<vector<int>> allCellsDistOrder(int rows, int cols, int rCenter, int cCenter) {
const int dr[4] = {1, 1, -1, -1}; // ↘ ↙ ↖ ↗ 四个方向
const int dc[4] = {1, -1, -1, 1};
int row = rCenter, col = cCenter;
vector<vector<int>> ans(rows*cols, vector<int>(2, 0));
ans[0][0] = row;
ans[0][1] = col;
int dist = 0;
int cnt = 1;
int n = rows*cols;
while(cnt < n){
--row; // 从上顶点开始
++dist; // 曼哈顿距离(每个方向的前进次数)
for(int i = 0; i < 4; ++i){ // 同一个i,即为相同的前进方向(分别先后到达上、右、下、左、上顶点)
for(int j = 1; j <= dist; ++j){
if(row >= 0 && row < rows && col >= 0 && col < cols){ // 排除方格外的点
ans[cnt][0] = row;
ans[cnt][1] = col;
++cnt;
}
row += dr[i]; // 按照当前方向移动
col += dc[i];
}
}
}
return ans;
}
};