算法基础——数组
1、预备
- 数组是存放在连续内存空间上的相同类型数据的集合,采用下标索引。
- 二维数组在内存空间是否连续取决于编程使用的语言。在python中可以通过id(),在c++中使用&取地址符进行观察。如在c++中
0x7ffee4065820 0x7ffee4065824 0x7ffee4065828 0x7ffee406582c 0x7ffee4065830 0x7ffee4065834
是int型占4字节,16进制的连续地址。说明二维数组在c++上是连续的。Python的解释器在执行程序时会负责虚拟内存地址的寻址操作,将程序中的变量、对象等分配到内存中的不同位置,并管理这些内存地址的分配和释放。Python的解释器使用了一种称为"引用计数"的机制来管理内存。每个对象都有一个引用计数,表示有多少个变量引用了该对象。当引用计数为0时,对象就会被垃圾回收。除了引用计数,Python的解释器还使用了其他的垃圾回收机制,如标记清除、分代回收等,来处理循环引用等特殊情况。
2、应用
2.1 二分查找 leetcode704
前提条件:有序数组、无重复元素
# 学前试写
class Solution:
def search(self, nums: List[int], target: int) -> int:
left=0
right=len(nums)-1
while left<=right:
mid=int((left+right)/2) # int向下取整
if nums[mid]==target:
return mid
if nums[mid]<target:
left=mid+1
if nums[mid]>target:
right=mid-1
return -1
- 循环不变式
二分法的两种写法,在于target取值区间定义,若定义于左闭右开,或左闭右闭,就要一直遵守。方法的选择决定了while条件语句中是否加入=号。
上方代码认为是左闭右闭,所以加入等号,因为相等时取nums[left]=nums[right]=target时有意义。同时if (nums[mid] > target) right 要赋值为 mid - 1,因为当前这个nums[mid]一定不是target,那么接下来要查找的左区间结束下标位置就是 mid - 1。
所以我们需要考虑清楚每一轮是在哪个区间取找,这点可以抽象成一个循环不变式。
【详见算法导论】待更
2.2 移除元素 leetcode27
- 暴力解法(两层for)
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
n=len(nums)
i=0
while i<n:
if nums[i]==val:
for j in range(i,n-1):
nums[j]=nums[j+1]
n-=1
i-=1
i+=1
return n
实际上还需要考虑数组内元素全等于val的特殊情形,此时i保持在0位置。
- 双指针
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
n=len(nums)
left=0
right=0
while right<n:
if nums[right]!=val:
nums[left]=nums[right]
left+=1
right+=1
return left
本解中循环不变性是:区间 [0,left)中的元素都不等于 val。当左右指针遍历完输入数组以后,left 的值就是输出数组的长度。
即left始终指向下一个可以加入输出数组元素的存储位置,right是挨个确认是不是可以加入的元素。
2.3 有序数组的平方 leetcode977
- 双指针 O(n)
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
ans=[]
left=0
right=len(nums)-1
while left<=right:
if abs(nums[left])>=abs(nums[right]):
ans.append(nums[left]**2)
left+=1
else:
ans.append(nums[right]**2)
right-=1
ans.reverse()
return ans
如果不用reverse,可以先生成一个长度为n的列表,从后往前加入元素也行。
在Python中,列表的元素在内存中是存储在不同的位置上的,而不是挨着存储的。列表是一种动态数组,它会根据需要自动扩展或缩小内存空间。列表的元素可以是不同类型的对象,所以它们可能会占据不同大小的内存空间
在Python的列表中,使用下标索引一个元素的时间复杂度是O(1)。这是因为列表在内存中是通过连续的内存块存储的,并且列表对象包含对第一个元素的引用,以及每个元素的地址偏移量。
- 暴力排序O(n+nlog(n))
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
ans=[]
n=len(nums)
for i in range(n):
ans.append(nums[i]**2)
ans.sort()
return ans
在Python中,sort方法的实现使用了一种叫做Timsort的排序算法。Timsort是一种混合排序算法,结合了合并排序(Merge Sort)和插入排序(Insertion Sort)的特性。
Timsort在排序过程中将列表分割成多个块,每个块称为run。然后,它使用插入排序将这些run进行排序,然后使用合并排序将排好序的run合并在一起,直到整个列表都被排序。
Timsort的时间复杂度为O(n log n),其中n是列表的大小。在最坏情况下,Timsort的时间复杂度为O(n log n),但在许多实际情况下,它可以达到O(n) 的性能,这是因为它利用了列表中已经有序或部分有序的特性。所以实际跑出来可能暴力要快一点。
2.4 长度最小的子数组 leetcode209
- 滑动窗口 O(n)
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
n=len(nums)
ans=n+1
sum=left=0
for right,x in enumerate(nums):
sum+=x
while sum-nums[left]>=target:
sum-=nums[left]
left+=1
if sum>=target:
ans=min(ans,right-left+1)
return ans if ans<n+1 else 0
enumerate介绍
在滑动窗口解法中,循环不变量(维持的窗口)是以right为右端点的满足条件的最小的子数组窗口(除了不存在情况)。能使用滑动窗口在于数组中存储的都是正整数。
- 暴力遍历 O(n^2)
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
n=len(nums)
ans=n+1
for i in range(n):
k=0
sum=0
for j in range(i,n):
sum+=nums[j]
k+=1
if sum>=target:
ans=min(ans,k)
break
if ans==n+1:
ans=0
return ans
2.5 螺旋矩阵Ⅱ leetcode59
- 按圈层模拟矩阵生成过程:注意循环不变性(对于本题,每圈四个阶段,每个阶段就把那一行/一列填i-1个)
时间复杂度O (n^2)
class Solution:
def generateMatrix(self, n: int) -> List[List[int]]:
nums=[[0]*n for i in range(n)]
i=n # 后面i是处理边界的变化量,故不直接用n替代。
num=1 # 记录填入的数值
while i>1: #处理第i圈,此时该层边长为i
k=(n-i)//2 # 求边长i时初始行数列数为多少
for q in range(i-1):
nums[k][k+q]=num
num+=1
for q in range(i-1):
nums[k+q][k+i-1]=num
num+=1
for q in range(i-1):
nums[k+i-1][k+i-1-q]=num
num+=1
for q in range(i-1):
nums[k+i-1-q][k]=num
num+=1
i-=2
if i==1:
nums[(n-1)//2][(n-1)//2]=n**2
return nums