0Pricing
DSA Interview Prep · 课时

数组基础与原地操作

复习索引和修改操作,了解数组面试中最常见的陷阱,例如下标偏移错误,以及迭代时修改列表。

数组基础与原地操作 是 CoddyKit 上的免费 DSA Interview Prep 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DSA Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DSA Interview Prep 课程共包含 4 节课。

数组作为连续内存

在底层,Python 列表由动态数组支持——这是一块连续的内存,其中的元素存储在连续的地址上。这种布局可以通过索引实现 O(1) 的随机访问:Python 能够立即计算出 address = base + index × element_size。在中间插入或删除元素需要移动后续的所有元素,成本为 O(n)。这种不对称性是数组面试中大多数权衡讨论的根源。

nums = [10, 20, 30, 40, 50]
# O(1) random access
print(nums[2])       # 30
print(nums[-1])      # 50

# O(1) append (amortised)
nums.append(60)
print(nums)          # [10,20,30,40,50,60]

# O(n) insert at beginning
nums.insert(0, 0)    # shifts all elements right
print(nums)          # [0,10,20,30,40,50,60]

差一错误:经典数组错误

差一错误是数组问题中最常见的错误答案来源。Python 使用从 0 开始的索引,因此最后一个有效索引是 len(arr) - 1。编写循环时,请使用最小有效输入(n=1 或 n=2)检查边界条件,以决定需要 < 还是 <=。提交前一定要用具体示例跟踪边界。

def find_max(nums):
    # Use len(nums)-1 as last index
    max_val = nums[0]              # safe if n >= 1
    for i in range(1, len(nums)):  # start at 1, not 0
        if nums[i] > max_val:
            max_val = nums[i]
    return max_val

print(find_max([3, 1, 4, 1, 5]))  # 5
print(find_max([7]))               # 7  (single element)
# Would crash if we accessed nums[len(nums)]

使用双指针原地反转

原地反转数组时,可以使用从两端开始、向中间移动的两个指针,并不断交换它们指向的元素,直到两者相遇。这需要 O(1) 的额外空间和 O(n) 的时间。条件 left < right(严格小于)可以确保偶数长度和奇数长度都正确——元素个数为奇数时,中间元素会自动保持原位。

def reverse_inplace(arr):
    left, right = 0, len(arr) - 1
    while left < right:
        arr[left], arr[right] = arr[right], arr[left]
        left  += 1
        right -= 1
    # Space: O(1)  Time: O(n)

a = [1, 2, 3, 4, 5]
reverse_inplace(a)
print(a)  # [5, 4, 3, 2, 1]

b = [1, 2, 3]
reverse_inplace(b)
print(b)  # [3, 2, 1]  middle element unchanged

原地旋转数组

将数组向右旋转 k 个位置,可以通过反转三个部分来原地完成:先反转整个数组,再反转前 k 个元素,最后反转剩余的 n-k 个元素。这样可以达到 O(n) 的时间和 O(1) 的空间,远好于使用切片和拼接所需的 O(n) 空间。一定要先计算 k 对 n 的模,以处理 k ≥ n 的情况。

def rotate(nums, k):
    n = len(nums)
    k %= n  # handle k >= n

    def rev(l, r):
        while l < r:
            nums[l], nums[r] = nums[r], nums[l]
            l += 1; r -= 1

    rev(0, n-1)    # reverse all
    rev(0, k-1)    # reverse first k
    rev(k, n-1)    # reverse rest

a = [1, 2, 3, 4, 5, 6, 7]
rotate(a, 3)
print(a)  # [5, 6, 7, 1, 2, 3, 4]

原地删除元素

原地删除重复值或目标值时,可以使用一个写指针跟踪下一个有效元素应写入的位置。读指针向前扫描;找到有效元素后,就将其复制到写入位置,并同时推进两个指针。这是 LeetCode 中“删除元素”“从有序数组中删除重复项”和“移动零”等问题的核心模式。

def remove_element(nums, val):
    write = 0
    for read in range(len(nums)):
        if nums[read] != val:
            nums[write] = nums[read]
            write += 1
    return write  # new length

nums = [3, 2, 2, 3]
new_len = remove_element(nums, 3)
print(nums[:new_len])  # [2, 2]

nums2 = [0, 1, 2, 2, 3, 0, 4, 2]
new_len2 = remove_element(nums2, 2)
print(nums2[:new_len2])  # [0, 1, 3, 0, 4]

移动零元素:读写指针

将所有零元素移到数组末尾,同时保持非零元素的顺序。读写指针方法会将每个非零元素放置到写入位置,然后用零元素填充末尾。另一种方法是将零元素向后交换,在无需第二次填充遍历的情况下保持顺序。两种方法的时间复杂度都是 O(n),空间复杂度都是 O(1)。

def move_zeroes(nums):
    write = 0
    # Move all non-zeroes to front
    for read in range(len(nums)):
        if nums[read] != 0:
            nums[write] = nums[read]
            write += 1
    # Fill rest with zeroes
    while write < len(nums):
        nums[write] = 0
        write += 1

a = [0, 1, 0, 3, 12]
move_zeroes(a)
print(a)  # [1, 3, 12, 0, 0]

原地平方并排序

给定一个已排序的整数数组(可能包含负数),请返回一个按排序顺序排列的平方值数组。朴素方法是先平方再排序:O(n log n)。最优的双指针方法利用已排序输入中最大平方值来自两端这一事实:比较最左端和最右端元素的绝对值,并从右向左填充结果,时间复杂度为 O(n)。

def sorted_squares(nums):
    n = len(nums)
    result = [0] * n
    left, right = 0, n - 1
    pos = n - 1  # fill from the right
    while left <= right:
        l_sq = nums[left]  ** 2
        r_sq = nums[right] ** 2
        if l_sq > r_sq:
            result[pos] = l_sq
            left += 1
        else:
            result[pos] = r_sq
            right -= 1
        pos -= 1
    return result

print(sorted_squares([-4, -1, 0, 3, 10]))
# [0, 1, 9, 16, 100]

查找枢轴并进行分区

荷兰国旗问题使用三个指针将数组原地划分为三个部分(小于、等于和大于枢轴)。这是快速排序的关键子步骤,也是 LeetCode 的“sort 颜色”问题的解法。维护这样的不变量:低位指针之前的元素都 < 枢轴,高位指针之后的元素都 > 枢轴,这推动了算法运行。

def sort_colors(nums):
    # Dutch national flag: 0s, 1s, 2s
    low, mid, high = 0, 0, len(nums) - 1
    while mid <= high:
        if nums[mid] == 0:
            nums[low], nums[mid] = nums[mid], nums[low]
            low += 1; mid += 1
        elif nums[mid] == 1:
            mid += 1
        else:
            nums[mid], nums[high] = nums[high], nums[mid]
            high -= 1  # don't advance mid: new nums[mid] unexamined

a = [2, 0, 2, 1, 1, 0]
sort_colors(a)
print(a)  # [0, 0, 1, 1, 2, 2]

迭代时修改数组元素

迭代时可以安全地修改元素值(例如乘以 -1 来标记已访问元素),但在循环过程中绝不能改变列表长度。一种安全的编码技巧是:暂时将两个值编码到同一个整数中(例如使用符号位),从而在不分配额外空间的情况下,为每个元素模拟一个额外的布尔值。这种技巧会出现在“查找数组中所有缺失的数字”等问题中。

def find_disappeared(nums):
    # Mark visited by negating the value at the index
    for n in nums:
        idx = abs(n) - 1
        if nums[idx] > 0:
            nums[idx] *= -1  # mark as seen
    # Indices with positive values are missing
    return [i + 1 for i, v in enumerate(nums) if v > 0]

print(find_disappeared([4, 3, 2, 7, 8, 2, 3, 1]))
# [5, 6]  -- O(n) time, O(1) extra space

数组面试模式检查清单

在编写任何数组问题的代码之前,请按以下思维清单逐项检查:

  • 数组是否已排序?(可以使用双指针、二分查找)
  • 元素是否有界(例如 1..n)?(可以使用基于索引的技巧)
  • 是否要求原地操作?(使用读写指针或交换)
  • 需要所有数对,还是只需要一个?(这会影响是否可以接受嵌套循环)
  • 边界情况:空数组、单个元素、所有值都相同
在编写代码前回答这些问题,可以显著减少调试时间。

def max_profit(prices):
    # Pattern: single scan, track running minimum
    # Time: O(n), Space: O(1)
    if not prices: return 0  # edge case: empty
    min_price = prices[0]
    max_prof  = 0
    for price in prices[1:]:  # start at index 1
        max_prof  = max(max_prof, price - min_price)
        min_price = min(min_price, price)
    return max_prof

print(max_profit([7, 1, 5, 3, 6, 4]))  # 5
print(max_profit([7, 6, 4, 3, 1]))     # 0

卡丹算法:最大子数组

卡丹算法可以在 O(n) 时间和 O(1) 空间内找到和最大的连续子数组。每一步都要决定是扩展当前子数组,还是开始一个新子数组:current = max(num, current + num)。如果 current + num 小于单独的 num,说明当前子数组正在拉低总和,此时应重新开始。整个过程中要持续记录全局最大值。

def max_subarray(nums):
    current = global_max = nums[0]
    for n in nums[1:]:
        current    = max(n, current + n)  # extend or restart
        global_max = max(global_max, current)
    return global_max

print(max_subarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]))
# 6  (subarray [4, -1, 2, 1])
print(max_subarray([-1, -2, -3]))
# -1  (all negative: take the least negative)

快速检查

测试您对本课“数据结构与算法——编程面试准备”相关概念的理解。

课程回顾

在本课中,您学习了:数组支持 O(1) 随机访问,但在中间插入和删除元素需要 O(n) 时间——了解这种不对称性有助于选择算法;读写指针模式可以在 O(n) 时间和 O(1) 空间内原地移除元素或移动值;以及符号位编码和将索引用作标记的技巧,可以为原本需要辅助数组的问题提供 O(1) 空间的解法。接下来,我们将学习前缀和与累计和。

常见问题解答

「数组基础与原地操作」课时是免费的吗?

是的 — 「数组基础与原地操作」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DSA Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 DSA Interview Prep 课程共包含 4 节课。

「数组基础与原地操作」这节课中我会学到什么?

复习索引和修改操作,了解数组面试中最常见的陷阱,例如下标偏移错误,以及迭代时修改列表。 你通过在浏览器中直接运行的动手代码来练习 DSA Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 DSA Interview Prep 需要有经验吗?

无需任何先前经验。CoddyKit 上的 DSA Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「数组基础与原地操作」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 DSA Interview Prep 课中编写并运行代码吗?

能。每节 DSA Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 数组基础与原地操作
  2. 前缀和与累计总和
  3. 双指针:从两端向中间
  4. 双指针:快慢指针
← 返回 DSA Interview Prep