每日算法 -- 用最少的步数将数组中的G和B移动到两侧

121 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第28天,点击查看活动详情

算法场景描述

给你一个数组arr,里面的元素只有字符GB,现在需要让GB移动到两侧,可以是:

  • G全部移动到左侧,B全部移动到右侧
  • G全部移动到右侧,B全部移动到左侧

元素的移动只能是通过相邻元素之间进行交换实现,问最少的移动次数是多少?

比如arr = ['B', 'B', 'G', 'G', 'G', 'B', 'G', 'B', 'B', 'G']

如果把G移动到左侧,B移动到右侧,最少的移动次数为14 而如果把B移动到左侧,G移动到右侧,最少的移动次数为11

所以答案是11

思路分析:双指针解法 -- 贪心思想

基于贪心的思想,假设现在要将G移动到左侧,B移动到右侧,那么第一个G移动到下标0位置后,下一个G移动到下标1处就行,并不一定要求移动到下标0处,因为这样做并没有什么意义,反而徒增交换次数

基于这样的思想,我们可以用两个指针pq,以G移动到左侧,B移动到右侧为例,p记录下一个G应当移动到左侧的下标,而q则负责遍历数组,遇到G就需要进行交换,但这里由于求的只是次数,所以不需要进行真正的交换,我们只需要计算以下两个指针之间的距离即代表需要交换的次数,然后累加到结果变量中即可

解法代码

export const solution = (arr: string[]): number => {
  // 双指针 p 和 q
  let p = 0
  let q = 0

  // 记录 G 移动到左侧,B 移动到右侧时的最少移动次数
  let res1 = 0
  // 记录 B 移动到左侧,G 移动到右侧时的最少移动次数
  let res2 = 0

  // 计算将 G 移动到左侧,B 移动到右侧时的最少移动次数
  while (q < arr.length) {
    if (arr[q] === 'G') {
      res1 += q - p
      // p 记录下一个 G 应当放置的位置
      p++
    }
    q++
  }

  // 计算将 B 移动到左侧,G 移动到右侧时的最少移动次数
  p = 0
  q = 0
  while (q < arr.length) {
    if (arr[q] === 'B') {
      res2 += q - p
      // p 记录下一个 G 应当放置的位置
      p++
    }
    q++
  }

  return Math.min(res1, res2)
}

单元测试

通过vitest编写一个简单的单元测试验证一下

import { solution } from './index'

describe('用最少的步数将数组中的G和B移动到两侧', () => {
  test('happy path', () => {
    const arr = ['B', 'B', 'G', 'G', 'G', 'B', 'G', 'B', 'B', 'G']
    const res = solution(arr)
    expect(res).toBe(11)
  })
})

重构优化

我们的解法代码中现在明显是有重复代码的,就是两个while循环,里面的代码重复率很高,可以抽离成一个函数进行优化

export const solution = (arr: string[]): number => {
  // 遍历数组计算答案
  const calcRes = (target: string) => {
    let p = 0
    let q = 0
    let res = 0

    while (q < arr.length) {
      if (arr[q] === target) {
        res += q - p
        // p 记录下一个 G 应当放置的位置
        p++
      }
      q++
    }

    return res
  }

  // 计算将 G 移动到左侧,B 移动到右侧时的最少移动次数
  const res1 = calcRes('G')

  // 计算将 B 移动到左侧,G 移动到右侧时的最少移动次数
  const res2 = calcRes('B')

  return Math.min(res1, res2)
}

可以看到代码现在简洁了不少,再跑一下单元测试发现依然能够通过,说明重构没影响到原来的算法正确性,重构成功!