数组理论基础(概念、二分法+双指针+滑动窗口+模拟行为)

数组理论基础

数组是非常基础的数据结构,是面试必问的基础而不失“优雅”的知识点。思维有时很简单,但是在具体实现上很容易陷入暴力循环的深渊里,虽然大部分时候暴力循环可以解决问题,但其复杂度往往在O(n^2)及以上,让心动的offer离我们越来越远,那么,如何能够做到在高效解决问题的同时占用较少的资源呢?本文将详细介绍数组面试题中的四大法宝—二分法+双指针法+滑动窗口法+模拟法。

1. 概念

什么是数组?

数组是存放在连续内存空间上相同类型数据的集合。
数组可以使用下标索引实现对目标元素的访问。如下图所示:
数组理论基础(概念、二分法+双指针+滑动窗口+模拟行为)_第1张图片
需要注意的是:

  • 数组下标从 0 开始。
  • 数组在内存空间的存储是连续的

正因为其在内存空间存放的连续性,所以我们在删除或增添数组的元素时,就难免的要大量移动其他元素的地址,虽然我们看不出来,但在底层中,确实是这样,举个例子:
删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:
数组理论基础(概念、二分法+双指针+滑动窗口+模拟行为)_第2张图片
而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。

数组的元素是不能删的,只能覆盖。
接下来看看二维数组,如下图所示:
数组理论基础(概念、二分法+双指针+滑动窗口+模拟行为)_第3张图片

接下来我们了解一下二维数组在空间中的存储方式,也是用一张图说明一下:

数组理论基础(概念、二分法+双指针+滑动窗口+模拟行为)_第4张图片
简单的理解,二维数组在内存中采用优先顺序进行存储,即先存储数组的第一行,再依次存储其他行。

数组问题的经典解决方法:

一、二分法 (详细介绍点击此链接)

当列表为升序不重复时,推荐使用二分查找。

LeetCode题目:题目链接

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

算法:

① 首先确定整个查找区间的中间位置 mid = (left + right) /2 。
② 用待查关键字值与中间位置的关键字值进行比较;若相等,则查找成功;若大于,则在后(右)半个区域继续进行折半查找;若小于,则在前(左)半个区域继续进行折半查找。
③ 对确定的缩小区域再按折半公式,重复上述步骤。最后,得到结果:要么查找成功, 要么查找失败。

二分法的两种实现方法:

1、左闭右闭 [left, right]

思路:

第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是 [left, right] (这个很重要非常重要)。

区间的定义这就决定了折半法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:

  • while (left <= right) 要使用 <= ,因为left == right是可以实际取到的,是有意义的,所以使用 <= 。
  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1 。

下面给出具体实现代码:

nums = [1,2,3,4,5,6,7,8,9,10,11,22,33,44,65,546,5432]
target = int(input("请输入目标数字"))
left = 0
right = len(nums) - 1
while (left <= right):
    middle = (left + right ) // 2
    if (nums[middle] > target):
        right = middle - 1
    elif (nums[middle] < target):
        left = middle + 1 
    elif(nums[middle] == target):
        print(middle)
        break  // 很重要,否则陷入无限循环
else:
     print(-1)  #未找到该元素

2、左闭右开 [left, right)

思路:

这种方法将 target 定义在一个在左闭右开的区间里,也就是[left, right)。需要注意,此时右边界是取不到的。需要注意下面两点:

  • while (left < right),这里使用 < ,因为left == right 是没有意义的,因为区间[left, right)无法被实际取到。
  • if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]。

下面给出具体实现代码:

nums = [1,2,3,4,5,6,7,8,9,10,11,22,33,44,65,546,5432]
target = int(input("请输入目标数字"))
left = 0
right = len(nums)
while left < right:
    middle = (left+right) // 2
    num = nums[middle]
    if num < target:
        left = middle + 1 #因为左侧是闭的,所以nums[left] 已经在上一轮被取到,所以下一轮循环需要将左侧标志位+1.
    elif num > target:
        right = middle
    else:
        print(middle)
        break // 很重要,否则陷入无限循环
else:
    print(-1) #未找到该元素

3、总结:

区间的定义就是不变量,那么在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则。折半查找中最重要的就是区间的选择和边界的确定。记住一点。考察区间是否存在,是否有实际意义,这样在写代码的时候就不会模糊。

二、双指针法(详细介绍点击此链接)

双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

  • 暴力解法时间复杂度:O(n^2)
  • 双指针时间复杂度:O(n)
LeetCode题目:题目链接

给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

思路:

本题要求不能新开辟空间,只能在原数组里进行修改,思路是定义两个指针指向数组的头部,快指针Fast 负责迅速遍历数组,慢指针Slow 负责将数组中与val不等的值原地覆盖,Slow每赋值一次就要加一,指向下一位置。

下面给出具体实现代码:

nums = [1,2,2,2,3,4,5,2,2]
val = int(input("请输入目标值:"))
Fast,Slow = 0,0
for Fast in range(len(nums)):
     if nums[Fast] != val:
          nums[Slow] = nums[Fast]
          Slow += 1
print(Slow)
print(nums)

#Output
#请输入目标值:2
#4
#[1, 3, 4, 5, 3, 4, 5, 2, 2]

3、总结:

双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法.它独特的结构使得数组不用开辟新的空间,直接在原数组上对数据进行修改,极大节省了内存,提高了运行效率。

三、滑动窗口法:

滑动窗口法,我更愿意称之为动态队列法。其思路就是:不断维护一个动态的队列,满足条件后,通过删除队头元素的方式不断调整子队列的起始位置和终止位置,从而得到我们想要的结果。

LeetCode题目:题目链接

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

思路:只用一个for循环,循环的索引表示滑动窗口的终止位置。关键就是这两个指针是如何移动的。定义j为初始位置,每次满足条件后执行 sum = sum - nums[j] #滑动窗口实现的关键:j为每次初始的值,当条件满足时,类似队列的结构,将队列的头元素删除,进行下一步判断。执行完后对j执行j += 1,使j指向下一位,完成滑动窗口的更新。

滑动窗口演示

窗口是满足其和 ≥ s 的长度最小的连续子数组。
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:可以发现滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。
可以发现滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)(滑动窗口法使用的前提是该问题可以使用双循环暴力求解)

下面给出具体实现代码:

'''方法一:双循环暴力求解 ,无技巧~
nums = [2,3,1,2,4,3]
target = 7
n = len(nums)
sum = 0
min_lenth = [0] * n
res = []
for i in range(n):
    count = 1
    sum = nums[i]
    if sum >= target:
        min_lenth[i] = count
        break
    else:
        for j in range(i+1,n):
             sum += nums[j]
             count += 1
             if sum >= target:
                  min_lenth[i] = count
                  break
for i in range(len(min_lenth)):
    if int(min_lenth[i]) > 0:
        res.append(min_lenth[i])
if len(res) != 0:
    print(min(res))
else:print(0)
'''
'''方法二:滑动窗口,只一次循环
nums = [2,3,1,2,4,3]
target = 7
n = len(nums)
res = float("inf")  # 定义一个无限大的数
print(res)
sum = 0
j = 0
for i in range(n):
    sum += nums[i]
    while(sum >= target):
        temp = i - j + 1
        res = min(temp,res)
        sum = sum -  nums[j] #滑动窗口实现的关键:j为每次初始的值,当条件满足时,类似队列的结构,将队列的头元素删除,进行下一步判断。
        j += 1
print(0) if res == float("inf") else print(res)
'''

四、模拟行为法:

模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,主要考察对代码的掌控能力。

思路:

主要要用到的是循环不变量原则,换句话说,就是在每一次子循环的处理中, 处理的子序列的长度和开闭区间的选择上都应该与前面保持一致。

LeetCode题目:题目链接

给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
数组理论基础(概念、二分法+双指针+滑动窗口+模拟行为)_第5张图片
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]

思路:

求解本题依然是要坚持循环不变量原则。
模拟顺时针画矩阵的过程:

  • 填充上行从左到右
  • 填充右列从上到下
  • 填充下行从右到左
  • 填充左列从下到上
    由外向内一圈一圈这么画下去。这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。
我以左闭右开区间为例来求解此问题,代码如下:
n = 3
res = [[0] * n for i in range(n)]
start_x, start_y = 0, 0
loop = n // 2
mid_point = n // 2 #n为奇数时,矩阵的中心点坐标值(中心的点坐标值横纵一样 )
count = 1
offset = 1 #每行的最后一个元素算入下一轮循环的首元素,且每循环完一整轮,值加一。
while loop :
    for j in range(start_y,n - offset):
        res[start_x][j] = count
        count += 1
    for i in range(start_x,n - offset):
        res[i][n - offset] = count
        count +=1
    for j in range(n - offset,start_y,-1):
        res[n - offset][j] = count
        count += 1
    for i in range(n- offset,start_x,-1):
        res[i][start_y] = count
        count +=1
    start_x += 1
    start_y += 1
    offset += 1
    loop -= 1
if n % 2 != 0:
    res[mid_point][mid_point] = count
print(res)

#Outout
# [[1, 2, 3], [8, 9, 4], [7, 6, 5]]

你可能感兴趣的:(leetcode,算法,python,学习)