中心下标:左边和等于总和减左减自己
先加出总和。走到每个位置,左边已经累过的数是 left,右边不用再加:总和减去 left 再减去自己。两边相等就停。
一趟求出 total。再从左到右维护 left。若 left == total − left − nums[i],i 就是中心下标;否则 left 加上 nums[i] 继续。找不到返回 -1。时间 O(n),空间 O(1)。
给你一个整数数组,找一个中心下标:它左边所有数的和,等于它右边所有数的和。中心自己不计入任何一边。左边或右边可以是空的,空的和按 0 算。有多个就返回最左边那个;没有就返回 -1。
主例 nums = [1, 7, 3, 6, 5, 6]。下标 3 上是 6,左边 1+7+3=11,右边 5+6=11,两边相等,答案是 3。
每个位置都把左右两段重新加一遍能做对,但是平方级。本文要回答:为什么右侧和不必另存数组,以及主例从下标 0 走到 3 时,left 和 right 分别是哪些数。
右侧和不用另算,用总和减
第一直觉是对每个下标 i,把左边 0..i-1 加一遍,右边 i+1..n-1 加一遍。主例 6 个数,大约再加几十次。结果能对,但左边那截每次只多一个数,却被从头重加。另一种错觉是先做完整前缀和数组再减:能把每次查询变成常数,可是多占了一整份数组,而本题每个下标只问一次、还是从左往右问。
缺的是一个随扫描更新的左侧和。先把整个数组加起来得到 total,主例 1+7+3+6+5+6=28。走到 i 时,left 表示严格在 i 左边的和;那么 i 右边的和一定是 total 减去 left 再减去 nums[i]——三部分拼起来就是全集,没有第四块。比较 left 和这个差,相等就返回 i。不相等,把 nums[i] 累进 left,继续往右。
为什么空间是常数:left 一个变量就够,不必留下每一格的前缀。为什么必须先算 total:没有全集,就无法用减法得到右侧。为什么先比较再累加:比较时 nums[i] 还不能进 left,它不属于任何一边;判定失败之后才把它交给未来的左侧。
用手走主例。i=0,左边空,left=0,right=28-0-1=27,不等,left 变成 1。i=1,left=1,right=28-1-7=20,不等,left 变成 8。演示下一帧跳到 i=2:left=1+7=8,right=28-8-3=17,仍不等,left 变成 11。
i=3 时 left=11,right=28-11-6=11,相等,返回 3。后面的 5、6 不用看,题目要最左边的中心。
下标 0 也可能是答案:左边和是 0,只要右边总和等于 0。全数组只有一个数时,左右都空,0=0,答案是 0。找不到时 left 会一路累完,返回 -1。不要把中心自己加进某一边,主例若把 6 算进左边会得到 17 对 11,错过唯一解。
为什么 O(1) 空间每个下标只检查一次,而且按从左到右的顺序检查。左侧和现场累加即可,右侧和由 total − left − 自己还原。前缀和数组能做,但本题用不上随机访问。
Go:单次扫描
func pivotIndex(nums []int) int {total := 0for _, x := range nums { total += x }left := 0for i, x := range nums {if left == total-left-x { return i }left += x}return -1}
1第一趟只求 total。主例 28。不要在第二趟里再改 total。
2先比较、后累加。比较时 x 是中心,不在 left 里。主例 i=3 时 left 仍是 11,28-11-6=11。
3从左往右第一处命中就是最左中心。扫完没有命中返回 -1。
总结
左边累加值对上「总和减左减自己」,就是中心。主例下标 3,11 对 11。
- 中心自己不计入任何一边。先比后加,顺序反了会错过主例。
- 右侧和不必另存。主例 i=0、2、3 的 right 分别是 27、17、11。
- 空侧和为 0。找不到返回 -1。额外只要两个整数变量。