Spiral Traversal

Sometimes we want to traverse in spiral, for example
Pasted image 20230506115407.png

Or

Pasted image 20230506115440.png

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.

Pasted image 20230507194341.png

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)

Pasted image 20230507194443.png

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:
Pasted image 20230507194721.png

After finishing this loop, we shift everything in side, so the boundary will look like as following:

Pasted image 20230507194800.png

And we complete the inner cycle loop:
Pasted image 20230507194922.png

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

Pasted image 20230507195405.png

Now after this, 4 pointers will go down to close the boundary:

Pasted image 20230507195530.png

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

Pasted image 20230507200127.png

Since we only loop from [left, right). We will go as following:

    1. [left, right) Pasted image 20230507200235.png
    1. Skip top to bottom because [top, bottom) results in no move
    1. Go [right, left)Pasted image 20230507200429.png
      As the result, it makes our middle element duplicate. Therefore, we need to check if we don't have [top, bottom), we would move right only once to cover the last element.

The same thing would happen for left and right vertically
Pasted image 20230507200903.png

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

Pasted image 20230507201349.png

So everytime we go, we go up to the last element as possible before changing the direction. So given the boundary like this:

Pasted image 20230507201449.png

Now we're going to move [left, right] (right inclusive).

Pasted image 20230507201721.png

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:

Pasted image 20230507201807.png

Pasted image 20230507201824.png

Next, we move right to left

Pasted image 20230507201858.png

And we go [right, left]

Pasted image 20230507201940.png

Next, move bottom up

Pasted image 20230507202003.png

Then we go [bottom, top]

Pasted image 20230507202029.png

Next we move left to to right and repeat from the start:

Pasted image 20230507202058.png
Pasted image 20230507202121.png
Pasted image 20230507202141.png
Pasted image 20230507202155.png
Pasted image 20230507202221.png
Pasted image 20230507202237.png

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
Pasted image 20230507203357.png

For this center element, the algorithm will still take it as we take [left, right]:

Pasted image 20230507203440.png

After doing that, top will go down as of the algorithm
Pasted image 20230507203519.png

Since the range [top, bottom] is not valid anymore, we move right to the left as part of the algorithm

Pasted image 20230507203616.png

As [right, left] is not a valid range anymore, we will not take any element here.

Pasted image 20230507203702.png

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
Pasted image 20230507203748.png

For problem 2, we will have to do edge case handling

Pasted image 20230507204047.png

Now, since we take [left, right] we will take the whole line.

Pasted image 20230507204316.png

Next, top will go down as of the algorithm

Pasted image 20230507204343.png

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

Pasted image 20230507204442.png

Now, we will take [right, left].

Pasted image 20230507204528.png

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