Spiral Traversal
Sometimes we want to traverse in spiral, for example
Or
For example: 54. Spiral Matrix
DFS based traversal
The idea is like going through a DFS in a map. For this we will have to mark each cell as we go.
For example:
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
VISITED = "X"
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
currentDirection = 0
m, n = len(matrix), len(matrix[0])
ans = []
if (m == 0 and n == 0): return ans
x,y = 0, 0
while (True):
ans.append(matrix[x][y])
matrix[x][y] = VISITED
dx, dy = directions[currentDirection % len(directions)]
if ((x + dx >= m or y + dy >= n) or matrix[x + dx][y + dy] == VISITED):
# Change the direction
currentDirection = (currentDirection + 1) % len(directions)
dx, dy = directions[currentDirection]
if ((x + dx >= m or y + dy >= n) or matrix[x + dx][y + dy] == VISITED):
# If after changing direction already and we still cannot go, we have finished the traversal
break
x += dx
y += dy
return ans
Time Complexity: $O(n \times m)$
Space Complexity: $O(1)$
However we need to modify the current array
4 pointers based algorithm
This is the same thing as Array rotation > Approach 2: Loop using 4 pointers.
Now there are 2 ways of doing it.
Pattern 1: Traverse only up to last element (not the whole line)
As we can see we only traverse up to the last element (we don't cross out the whole role). Doing this will look intuiative. After the whole traversal, we shift the boundary inside.
So the first iteration will look like this:
After finishing this loop, we shift everything in side, so the boundary will look like as following:
And we complete the inner cycle loop:
Problem:
However, doing this will create some edge cases:
Problem 1: Miss out the center element
Since we are only traversing from [left, right)
(not including right), the first edge cases we have is we will leave out the center element. For example: ^b9ca21
Now after this, 4 pointers will go down to close the boundary:
However, since we're only looping from [left, right)
. We will not take this element.
Problem 2: Middle line edge case
For example if we have a middle line:
Next we will close in the boundaries
Since we only loop from [left, right)
. We will go as following:
[left, right)
- Skip top to bottom because
[top, bottom)
results in no move
- Skip top to bottom because
- Go
[right, left)
As the result, it makes our middle element duplicate. Therefore, we need to check if we don't have[top, bottom)
, we would moveright
only once to cover the last element.
- Go
The same thing would happen for left
and right
vertically
The code
class App:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if len(matrix) == 0: return []
if len(matrix) == 1: return matrix[0]
result = []
left, right = 0, len(matrix[0]) - 1
top, bottom = 0, len(matrix) - 1
while left <= right and top <= bottom:
for i in range(right - left):
result.append(matrix[top][left + i])
for i in range(bottom - top):
result.append(matrix[top + i][right])
for i in range(right - left):
result.append(matrix[bottom][right - i])
# edge case middle line horizontal
if (bottom == top):
break
for i in range(bottom - top):
result.append(matrix[bottom - i][left])
# edge case middle line vertical
if (left == right):
break
left += 1
top += 1
bottom -= 1
right -= 1
# Handle the center edge case
if len(matrix) == len(matrix[0]) and len(matrix) % 2 != 0:
mid = len(matrix) // 2
result.append(matrix[mid][mid])
return result
Pattern 2 (Recommend): Traverse the whole role and move boundary in order
So everytime we go, we go up to the last element as possible before changing the direction. So given the boundary like this:
Now we're going to move [left, right]
(right inclusive).
Now since we've already crossed the corner element of [right, top]
, we will need to move top
down and go from top to bottom:
Next, we move right
to left
And we go [right, left]
Next, move bottom
up
Then we go [bottom, top]
Next we move left
to to right and repeat from the start:
Edge cases
For problem 1, this wont happen as the algorithm itself will take [left, right]
or `[top, bottom]:
So just before we finish getting all the around, this is what we have
For this center element, the algorithm will still take it as we take [left, right]
:
After doing that, top
will go down as of the algorithm
Since the range [top, bottom]
is not valid anymore, we move right
to the left as part of the algorithm
As [right, left]
is not a valid range anymore, we will not take any element here.
Bottom
will go up as part of the algorithm, however the range [bottom, top]
is not valid anymore so we're not taking anything here.
Similarly for left
For problem 2, we will have to do edge case handling
Now, since we take [left, right]
we will take the whole line.
Next, top
will go down as of the algorithm
the range [top, bottom]
is negative so we're not adding anything here. Next we move right to the left as part of the algorithm
Now, we will take [right, left]
.
However as you see, there will be a problem here. This happen when top > bottom
, as a result we only have 1 line and the previous loop from [left, right]
should went through this already.
Similar problem in vertical case. Therefore it's important to terminate the algorithm when there is 1 line left in the second half when going from right to left
or bottom to top
The code
class App:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if len(matrix) == 0: return []
if len(matrix) == 1: return matrix[0]
result = []
left, right = 0, len(matrix[0]) - 1
top, bottom = 0, len(matrix) - 1
while left <= right and top <= bottom:
for i in range(right - left + 1):
result.append(matrix[top][left + i])
top += 1
for i in range(bottom - top + 1):
result.append(matrix[top + i][right])
right -= 1
if left > right or top > bottom:
# edge case middle line
break
for i in range(right - left + 1):
result.append(matrix[bottom][right - i])
bottom -= 1
for i in range(bottom - top + 1):
result.append(matrix[bottom - i][left])
left += 1
return result