[开发中遇到的算法] 均分数组

业务背景

最近我需要写并发rpc的负载均衡(某种意义上的吧),遇到很有意思的问题。需求如下:

  • 下游固定死最多一次请求100个。
  • 比如要请求101个时要拆两个请求并发rpc,并等待两个请求都返回后拼装成一个结果返回。
  • 拆成51个+50个发出请求比拆成100+1个好,因为预期处理时间最低。

就是需要一个函数干这个平均拆分的事。

函数定义

这里给一个Go的定义:

func EvenlySplitInt64(arr []int64, batchMaxSize int) [][]int64

需求:

  1. 把arr中的数值拆分到数个数组中(拆分成数个rpc请求)
  2. 每个数组中的元素个数不得大于batchMaxSize(最多一次请求batchMaxSize个)
  3. arr中的每个元素在结果数组中出现有且仅有1次(不能漏请求或者多请求)
  4. 拆分的数组尽可能的少(rpc数量尽可能少)
  5. 结果数组中的个数尽可能平均,或说任意两个数组的元素个数最多差1(每个并发的请求时间预期接近)

额外要求:

  1. 算法稳定,同一个arr的输出结果不变。arr中每个元素的排序不变,或说将结果数组头尾连起来后每个元素的排列位置和原先一致。
  2. 结果数组头重脚轻,或说前面的数组的元素个数不少于后面的数组

单测

上面可能不好理解,看看单测就知道什么意思了。

func TestEvenlySplitInt64(t *testing.T) {
        tests := []struct {
                name         string
                arr          []int64
                batchMaxSize int
                want         [][]int64
        }{
                {name: "case0", arr: []int64{1}, batchMaxSize: 1, want: [][]int64{{1}}},
                {name: "case1", arr: []int64{1}, batchMaxSize: 2, want: [][]int64{{1}}},
                {name: "case2", arr: []int64{}, batchMaxSize: 2, want: [][]int64{}},
                {name: "case3", arr: []int64{1, 2, 3}, batchMaxSize: 1, want: [][]int64{{1}, {2}, {3}}},
                {name: "case4", arr: []int64{1, 2, 3}, batchMaxSize: 2, want: [][]int64{{1, 2}, {3}}},
                {name: "case5", arr: []int64{1, 2, 3, 4}, batchMaxSize: 2, want: [][]int64{{1, 2}, {3, 4}}},
                {name: "case6", arr: []int64{1, 2, 3, 4, 5}, batchMaxSize: 2, want: [][]int64{{1, 2}, {3, 4}, {5}}},
                {name: "case7", arr: []int64{1, 2, 3, 4, 5, 6}, batchMaxSize: 2, want: [][]int64{{1, 2}, {3, 4}, {5, 6}}},
                {name: "case8", arr: []int64{1, 2, 3, 4, 5, 6}, batchMaxSize: 3, want: [][]int64{{1, 2, 3}, {4, 5, 6}}},
                {name: "case9", arr: []int64{1, 2, 3, 4, 5, 6, 7}, batchMaxSize: 3, want: [][]int64{{1, 2, 3}, {4, 5}, {6, 7}}},
                {name: "case10", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8}, batchMaxSize: 3, want: [][]int64{{1, 2, 3}, {4, 5, 6}, {7, 8}}},
                {name: "case11", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8}, batchMaxSize: 4, want: [][]int64{{1, 2, 3, 4}, {5, 6, 7, 8}}},
                {name: "case12", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8}, batchMaxSize: 5, want: [][]int64{{1, 2, 3, 4}, {5, 6, 7, 8}}},
                {name: "case13", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8}, batchMaxSize: 6, want: [][]int64{{1, 2, 3, 4}, {5, 6, 7, 8}}},
                {name: "case14", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8}, batchMaxSize: 7, want: [][]int64{{1, 2, 3, 4}, {5, 6, 7, 8}}},
                {name: "case15", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8}, batchMaxSize: 8, want: [][]int64{{1, 2, 3, 4, 5, 6, 7, 8}}},
                {name: "case16", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8}, batchMaxSize: 9, want: [][]int64{{1, 2, 3, 4, 5, 6, 7, 8}}},
                {name: "case17", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}, batchMaxSize: 1, want: [][]int64{{1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}}},
                {name: "case18", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}, batchMaxSize: 2, want: [][]int64{{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9}}},
                {name: "case19", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}, batchMaxSize: 3, want: [][]int64{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}},
                {name: "case20", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}, batchMaxSize: 4, want: [][]int64{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}},
                {name: "case21", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}, batchMaxSize: 5, want: [][]int64{{1, 2, 3, 4, 5}, {6, 7, 8, 9}}},
                {name: "case22", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}, batchMaxSize: 6, want: [][]int64{{1, 2, 3, 4, 5}, {6, 7, 8, 9}}},
                {name: "case23", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}, batchMaxSize: 7, want: [][]int64{{1, 2, 3, 4, 5}, {6, 7, 8, 9}}},
                {name: "case24", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}, batchMaxSize: 8, want: [][]int64{{1, 2, 3, 4, 5}, {6, 7, 8, 9}}},
                {name: "case25", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}, batchMaxSize: 9, want: [][]int64{{1, 2, 3, 4, 5, 6, 7, 8, 9}}},
		        {name: "case26", arr: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, batchMaxSize: 3, want: [][]int64{{1, 2, 3}, {4, 5, 6}, {7, 8}, {9, 10}}},
        }
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
                        if got := EvenlySplitInt64(tt.arr, tt.batchMaxSize); !reflect.DeepEqual(got, tt.want) {
                                t.Errorf("EvenlySplitInt64() = %v, want %v", got, tt.want)
                        }
                })
        }
}

最后写的这个,1-10要分成 1,2,3和4,5,6和7,8和9,10是最能体现关键要点的。

如果你想的是一开始就算好每个数组分几个值,那肯定是错的,再想想吧。

各位可以试试自己写一写,练练生锈的脑袋瓜。我写起来也是思考了好一阵子才搞定的。
要跑过单测哦。

参考答案

1

我写完后和同事讨论,他给出了个我也没看懂的算法。但是反正可以跑通单测。感兴趣的可以自己研究研究。

func EvenlySplitInt64(arr []int64, max int) [][]int64 {
	ceil := math.Ceil(float64(len(arr)) / float64(max))
	result := make([][]int64, int(ceil))
	for i, idx := 0, 0; ; i++ {
		if idx == len(arr) {
			break
		}
		if i == len(result) {
			i = 0
		}
		result[i] = append(result[i], arr[idx])
		idx++
		print(render.Render(result))
		time.Sleep(time.Second * 2)
	}
	var idx int
	for i := range result {
		for j := range result[i] {
			result[i][j] = arr[idx]
			idx++
		}
	}
	return result
}

时间复杂度是O(2N),不够geek。

2

最终写出来的是个时间复杂度O(N/m)的。应该已经是极限了,有更极致的算法欢迎交流:

func EvenlySplitInt64(arr []int64, batchMaxSize int) [][]int64 {
	if batchMaxSize <= 0 {
		panic("invalid batchMaxSize")
	}
	if len(arr) <= 0 {
		return [][]int64{}
	}
	slotCnt := (len(arr)-1)/batchMaxSize + 1
	result := make([][]int64, slotCnt)
	for i := 0; i < slotCnt; i++ {
		slotSize := (len(arr)-1)/(slotCnt-i) + 1
		result[i] = arr[:slotSize]
		arr = arr[slotSize:]
	}
	return result
}

用到的是贪心算法。

  1. 最少要分几个数组一开始就能算好。
  2. 一个个数组往后填,每次填的个数取当前最优,即平均数,为了头重脚轻,要向上取整。
  3. 最终出来的结果就是最均匀的。

结语

同事一起参与讨论后,觉得这道题很适合用在他的面试中。所以,搞不好你在面试我厂的时候就会碰上这题哦。

怎么样,你聪明的小脑袋做出了这道题了么?

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值