数组理论基础

162 阅读20分钟

image.png

数据结构的世界如同一堵厚实的砖墙。

数组的砖块整齐排列,逐个紧贴。链表的砖块分散各处,连接的藤蔓自由地穿梭于砖缝之间。

数组理论基础

数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力

也就是说,想法很简单,但实现起来 可能就不是那么回事了。

首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题

数组是存放在连续内存空间上的相同类型数据的集合。

数组可以方便的通过下标索引的方式获取到下标对应的数据。

举一个字符数组的例子,如图所示:

算法通关数组.png 需要两点注意的是

  • 数组下标都是从0开始的。
  • 数组内存空间的地址是连续的

正是因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。

例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:

算法通关数组1.png

数组的元素是不能删的,只能覆盖。

那么二维数组直接上图,大家应该就知道怎么回事了

算法通关数组2.png

那么二维数组在内存的空间地址是连续的么?

不同编程语言的内存管理是不一样的,以C++为例,在C++中二维数组是连续分布的。

我们来做一个实验,C++测试代码如下:

void test_arr() {
    int array[2][3] = {
		{0, 1, 2},
		{3, 4, 5}
    };
    cout << &array[0][0] << " " << &array[0][1] << " " << &array[0][2] << endl;
    cout << &array[1][0] << " " << &array[1][1] << " " << &array[1][2] << endl;
}

int main() {
    test_arr();
}

测试地址为

0x7ffee4065820 0x7ffee4065824 0x7ffee4065828
0x7ffee406582c 0x7ffee4065830 0x7ffee4065834

注意地址为16进制,可以看出二维数组地址是连续一条线的。

一些录友可能看不懂内存地址,我就简单介绍一下, 0x7ffee4065820 与 0x7ffee4065824 差了一个4,就是4个字节,因为这是一个int型的数组,所以两个相邻数组元素地址差4个字节。

0x7ffee4065828 与 0x7ffee406582c 也是差了4个字节,在16进制里8 + 4 = c,c就是12。

如图:

算法通关数组3.png

所以可以看出在C++中二维数组在地址空间上是连续的

像Java是没有指针的,同时也不对程序员暴露其元素的地址,寻址操作完全交给虚拟机。

所以看不到每个元素的地址情况,这里我以Java为例,也做一个实验。

public static void test_arr() {
    int[][] arr = {{1, 2, 3}, {3, 4, 5}, {6, 7, 8}, {9,9,9}};
    System.out.println(arr[0]);
    System.out.println(arr[1]);
    System.out.println(arr[2]);
    System.out.println(arr[3]);
}

输出的地址为:

[I@7852e922
[I@4e25154f
[I@70dea4e
[I@5c647e05

这里的数值也是16进制,这不是真正的地址,而是经过处理过后的数值了,我们也可以看出,二维数组的每一行头结点的地址是没有规则的,更谈不上连续。

所以Java的二维数组可能是如下排列的方式:

算法通关数组4.png

数组的优点与局限性

数组存储在连续的内存空间内,且元素类型相同。这种做法包含丰富的先验信息,系统可以利用这些信息来优化数据结构的操作效率。

  • 空间效率高:数组为数据分配了连续的内存块,无须额外的结构开销。
  • 支持随机访问:数组允许在 O(1) 时间内访问任何元素。
  • 缓存局部性:当访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓存来提升后续操作的执行速度。

连续空间存储是一把双刃剑,其存在以下局限性。

  • 插入与删除效率低:当数组中元素较多时,插入与删除操作需要移动大量的元素。
  • 长度不可变:数组在初始化后长度就固定了,扩容数组需要将所有数据复制到新数组,开销很大。
  • 空间浪费:如果数组分配的大小超过实际所需,那么多余的空间就被浪费了。

704. 二分查找

力扣题目链接(opens new window)

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9     
输出: 4       
解释: 9 出现在 nums 中并且下标为 4     

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2     
输出: -1        
解释: 2 不存在 nums 中因此返回 -1        

提示:

  • 你可以假设 nums 中的所有元素是不重复的。
  • n 将在 [1, 10000]之间。
  • nums 的每个元素都将在 [-9999, 9999]之间。
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left,right=0,len(nums)      #[left,right)
        while left<right:
            middle=left+(right-left)//2
            if nums[middle]<target:
                left=middle+1
            elif nums[middle]>target:
                right=middle
            else:
                return middle
        return -1

#二分查找」通过不断缩小搜索区间的范围,直到找到目标元素。
#左闭右闭区间[left,right]
#左闭右开区间[left,right)

35.搜索插入位置

力扣题目链接(opens new window)

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

示例 1:

  • 输入: [1,3,5,6], 5
  • 输出: 2

示例 2:

  • 输入: [1,3,5,6], 2
  • 输出: 1

示例 3:

  • 输入: [1,3,5,6], 7
  • 输出: 4

示例 4:

  • 输入: [1,3,5,6], 0
  • 输出: 0
#   第一种二分法: [left, right]左闭右闭区间
class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1

        while left <= right:
            middle = (left + right) // 2

            if nums[middle] < target:  # target 在右区间,所以[middle + 1, right]
                left = middle + 1
            elif nums[middle] > target: # target 在左区间,所以[left, middle - 1]
                right = middle - 1
            else:
                return middle
         # 分别处理如下四种情况
        # 目标值在数组所有元素之前  [0, -1]
        # 目标值等于数组中某一个元素  return middle;
        #目标值插入数组中的位置 [left, right],return  right + 1
         #目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1
        return right + 1


34. 在排序数组中查找元素的第一个和最后一个位置

力扣链接(opens new window)

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:你可以设计并实现时间复杂度为 O(logn)O(\log n) 的算法解决此问题吗?

示例 1:

  • 输入:nums = [5,7,7,8,8,10], target = 8
  • 输出:[3,4]

示例 2:

  • 输入:nums = [5,7,7,8,8,10], target = 6
  • 输出:[-1,-1]

示例 3:

  • 输入:nums = [], target = 0
  • 输出:[-1,-1]
class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        def search_R(nums, target):
            left, right = 0, len(nums) - 1
            while left <= right:
                middle = (left + right) // 2
                if nums[middle] == target:
                    if middle == len(nums) - 1 or nums[middle + 1] > target:
                        return middle
                if nums[middle] <= target:
                    left = middle + 1  # 范围缩小到 [middle+1, right]
                else:
                    right = middle - 1  # 范围缩小到 [left, middle-1]
            return -1

        def search_L(nums, target):
            left, right = 0, len(nums) - 1
            while left <= right:
                middle = (left + right) // 2
                if nums[middle]==target:
                    if middle== 0 or nums[middle - 1] < target :
                        return middle
                if nums[middle] < target:
                    left = middle + 1  # 范围缩小到 [middle+1, right]
                else:
                    right = middle - 1  # 范围缩小到 [left, middle-1]
            return -1

        l = search_L(nums, target)
        r = search_R(nums, target)
        return [l, r]

#扎扎实实的写两个二分,分别找左边界和右边界

69. x 的平方根 

给你一个非负整数 x ,计算并返回 x 的 算术平方根 。

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

注意: 不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。

示例 1:

输入: x = 4
输出: 2

示例 2:

输入: x = 8
输出: 2
解释: 8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
class Solution:
    def mySqrt(self, x: int) -> int:
        left,right=0,x
        ans=-1
        while left<=right:
            middle=(left+right)//2
            if middle*middle<=x:
                ans=middle
                left=middle+1
            else:
                right=middle-1
        return ans

367. 有效的完全平方数

给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。

完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。

不能使用任何内置的库函数,如  sqrt 。

示例 1:

输入: num = 16
输出: true
解释: 返回 true ,因为 4 * 4 = 164 是一个整数。

示例 2:

输入: num = 14
输出: false
解释: 返回 false ,因为 3.742 * 3.742 = 143.742 不是一个整数。
class Solution:
    def isPerfectSquare(self, num: int) -> bool:
        left,right=0,num

        while left<=right:
            middle=(left+right)//2
            if middle*middle==num:
                return True
            elif middle*middle<num:
                left=middle+1
            else:
                right=middle-1
        return False

27. 移除元素

力扣题目链接(opens new window)

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。

示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。

你不需要考虑数组中超出新长度后面的元素。

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        #数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。
        #双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
        slow=0
        fast=0
        size=len(nums)
        while fast<size:
            #slow是用来收集不等于val的值
            if nums[fast]!=val:
                nums[slow]=nums[fast]
                slow+=1
            fast+=1
        return slow

26. 删除有序数组中的重复项

给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

  • 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
  • 返回 k 。

判题标准:

系统会用下面的代码来测试你的题解:

int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案

int k = removeDuplicates(nums); // 调用

assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
    assert nums[i] == expectedNums[i];
}

如果所有断言都通过,那么您的题解将被 通过。 

示例 1:

输入: nums = [1,1,2]
输出: 2, nums = [1,2,_]
解释: 函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。

示例 2:

输入: nums = [0,0,1,1,1,2,2,3,3,4]
输出: 5, nums = [0,1,2,3,4]
解释: 函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

快慢指针

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        slow,fast=0,1
        while fast<len(nums):
            if nums[fast]!=nums[slow]:
                slow+=1
                nums[slow]=nums[fast]
            fast+=1

        return slow+1

283. 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:

输入: nums = [0]
输出: [0]

快慢指针

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
    
        """
        Do not return anything, modify nums in-place instead.
        """
        
        
        slow,fast=0,0
        while fast<len(nums):
            if nums[fast]!=0:
                nums[slow]=nums[fast]
                slow+=1
            fast+=1

        while slow<len(nums):
            nums[slow]=0
            slow+=1
        

844. 比较含退格的字符串

给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。

注意: 如果对空文本输入退格字符,文本继续为空。  

示例 1:

输入: s = "ab#c", t = "ad#c"
输出: true
解释: s 和 t 都会变成 "ac"

示例 2:

输入: s = "ab##", t = "c#d#"
输出: true
解释: s 和 t 都会变成 ""

示例 3:

输入: s = "a#c", t = "b"
输出: false
解释: s 会变成 "c",但 t 仍然是 "b"
class Solution:
    def backspaceCompare(self, s: str, t: str) -> bool:
        def get_str(s):
            res = []
            for i in s:
                if i != '#':
                    res.append(i)
                elif i=='#' and len(res) > 0:
                    res.pop()
 #如果字符是#且res列表不为空,则从res列表中移除最后一个元素(模拟退格操作)
            return str(res)
        return get_str(s) == get_str(t)

977. 有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1:

输入: nums = [-4,-1,0,3,10]
输出: [0,1,9,16,100]
解释: 平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例 2:

输入: nums = [-7,-3,2,3,11]
输出: [4,9,9,49,121]

相向双指针。

数组其实是有序的, 只不过负数平方之后可能成为最大数了。

那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。

此时可以考虑双指针法了,i指向起始位置,j指向终止位置。

定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。

如果A[i] * A[i] < A[j] * A[j] 那么result[k--] = A[j] * A[j]; 。

如果A[i] * A[i] >= A[j] * A[j] 那么result[k--] = A[i] * A[i]; 。

如动画所示:

class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        #相向双指针
        n=len(nums)
        i,j=0,n-1
        ans=[0]*n
        for k in range(n-1,-1,-1):
            x=nums[i]*nums[i]
            y=nums[j]*nums[j]
            if x>y:
                ans[k]=x
                i+=1
            else:
                ans[k]=y
                j-=1

        return ans  

209.长度最小的子数组

力扣题目链接(opens new window)

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

示例:

  • 输入:s = 7, nums = [2,3,1,2,4,3]
  • 输出:2
  • 解释:子数组 [4,3] 是该条件下的长度最小的子数组。

提示:

  • 1 <= target <= 10^9
  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^5

滑动窗口

接下来就开始介绍数组操作中另一个重要的方法:滑动窗口

所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果

在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。

那么滑动窗口如何用一个for循环来完成这个操作呢。

首先要思考 如果用一个for循环,那么应该表示 滑动窗口的起始位置,还是终止位置。

如果只用一个for循环来表示 滑动窗口的起始位置,那么如何遍历剩下的终止位置?

此时难免再次陷入 暴力解法的怪圈。

所以 只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置。

那么问题来了, 滑动窗口的起始位置如何移动呢?

这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:

209.长度最小的子数组

最后找到 4,3 是最短距离。

其实从动画中可以发现滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。

在本题中实现滑动窗口,主要确定如下三点:

  • 窗口内是什么?
  • 如何移动窗口的起始位置?
  • 如何移动窗口的结束位置?

窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。

窗口的起始位置如何移动:如果当前窗口的值大于等于s了,窗口就要向前移动了(也就是该缩小了)。

窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。

解题的关键在于 窗口的起始位置如何移动,如图所示:

leetcode_209

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        left,right=0,0
        min_len=float('inf')  #正无穷
        cur_num=0

        while right<len(nums):
            cur_num+=nums[right]

            while cur_num>=target:
                min_len=min(min_len,right-left+1)
                cur_num-=nums[left]
                left+=1
            right+=1

        return min_len if min_len!=float('inf') else 0

#滑动窗口

#所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果

#窗口就是 满足其和 ≥ target的长度最小的 连续 子数组。

904. 水果成篮

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

  • 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
  • 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
  • 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。

给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。

 

示例 1:

输入: fruits = [1,2,1]
输出: 3
解释: 可以采摘全部 3 棵树。

示例 2:

输入: fruits = [0,1,2,2]
输出: 3
解释: 可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。

示例 3:

输入: fruits = [1,2,3,2,2]
输出: 4
解释: 可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。

示例 4:

输入: fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出: 5
解释: 可以采摘 [1,2,1,1,2] 这五棵树。
class Solution:
    def totalFruit(self, fruits: List[int]) -> int:
        i,j=0,0
        ans=0
        n=len(fruits)
        dic=defaultdict(int)
        classCount=0

        while j<n:
            v1=fruits[j]
            if dic[v1]==0:
                classCount+=1
            dic[v1]+=1

            while classCount>2:
                v2=fruits[i]
                if dic[v2]==1:
                    classCount-=1
                dic[v2]-=1
                i+=1

            ans=max(ans,j-i+1)
            j+=1
        return ans

76. 最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

 

示例 1:

输入: s = "ADOBECODEBANC", t = "ABC"
输出: "BANC"
解释: 最小覆盖子串 "BANC" 包含来自字符串 t 的 'A''B''C'

示例 2:

输入: s = "a", t = "a"
输出: "a"
解释: 整个字符串 s 是最小覆盖子串。

示例 3:

输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if len(s)<len(t):
            return ""

        dic_s=defaultdict(int)
        dic_t=defaultdict(int)

        for i in t:
            dic_t[i]+=1

        start,end=0,0
        min_len=float('inf')
        min_start=0
        have=0   #满足t字符串的个数,当have==3时符合条件
        total=len(t)

        while end<len(s):
            char_end=s[end]
            dic_s[char_end]+=1

            if char_end in dic_t and dic_s[char_end]<=dic_t[char_end]:
                have+=1
            
            while have==total:
                if end-start+1<min_len:
                    min_len=end-start+1
                    min_start=start

                char_start=s[start]
                dic_s[char_start]-=1
                if char_start in dic_t and dic_s[char_start]<dic_t[char_start]:
                    have-=1
                start+=1

            end+=1

        if min_len==float('inf'):
            return ""
        else:
            return s[min_start:min_start+min_len] 

59.螺旋矩阵II

力扣题目链接(opens new window)

给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。

示例:

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

class Solution:
    def generateMatrix(self, n: int) -> List[List[int]]:
        nums=[[0]*n for _ in range(n)]   #初始化矩阵
        startx,starty=0,0
        loop=n//2                    #模拟的圈数
        mid=n//2
        count=1                     #用来赋值

#offset 需要控制每一条边遍历的长度,每次循环右边界收缩一位
        for offset in range(1,loop+1):
            for i in range(startx,n-offset):
                nums[startx][i]=count
                count+=1
            
            for i in range(starty,n-offset):
                nums[i][n-offset]=count
                count+=1

            for i in range(n-offset,startx,-1):
                nums[n-offset][i]=count
                count+=1

            for i in range(n-offset,starty,-1):
                nums[i][starty]=count
                count+=1

            startx+=1
            starty+=1

        if n%2==1:
            nums[mid][mid]=n**2
        return nums

'''
模拟顺时针画矩阵的过程:

填充上行从左到右
填充右列从上到下
填充下行从右到左
填充左列从下到上
'''

54. 螺旋矩阵

给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

示例 1:

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

示例 2:

输入: matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出: [1,2,3,4,8,12,11,10,9,5,6,7]
class Solution:
    def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
        top=0
        bottom=len(matrix)-1
        left=0
        right=len(matrix[0])-1
        ans=[]

        while True:
            for i in range(left,right+1):
                ans.append(matrix[top][i])

            top+=1
            if top>bottom: break

            for i in range(top,bottom+1):
                ans.append(matrix[i][right])

            right-=1
            if left>right: break

            for i in range(right,left-1,-1):
                ans.append(matrix[bottom][i])

            bottom-=1
            if top>bottom: break

            for i in range(bottom,top-1,-1):
                ans.append(matrix[i][left])

            left+=1
            if left>right: break

        return ans