计算时间重叠次数的优化

有这样一种场景,在一个直播间内,在一个较长的时间内,有人加入群聊,也有人离开,在直播间的整个生命周期内,会产生很多的访问记录,这些记录都有进入时间和离开时间,业务场景是需要计算在直播间存续期间,在线人数最多的时间段,以及人数。

由于项目中直播间实际会控制最大人数在6人,而直播间存续时长一般也不会超过24小时,所以开始时偷了点懒,采用暴力方式处理。

暴力解法

遍历所有的时间段,将时间段按照起始/结束时间分割,比如用户A在线时间是(9:15-9:20),用户B是(9:18-9:30),两者在9:18-9:20期间同时在线,分割时间段后变成了(9:15-9:18),(9:18-9:20),(9:20-9:30),分割过程计算这三个时间段的在线人数分别为1、2、1。后面输入的时间段重复上述过程,遍历已经分割完的子时间段,再与些个时间段进行分割,并同时累加在线人数。当所有时间段分割完成后,遍历所有分割完的时间段对应的在线人数,最后输出时间段和在线人数。

显然这是一个N²复杂度的算法,如果每一个时间段,都会被其他所有时间段分割,那么输入的n个时间段最终会产生2n-1个时间段,所以实际上会比N²还要耗时,开始以为偷懒的暴力方法,事实证明,一点也不省事儿,实现代码如下

type timeSectionList struct {
	tss []*timeSection
}
type timeSection struct {
	s       int64 //开始时间
	e       int64 // 结束时间
	overlap int   // 当前时间段重叠次数
}

func (this *timeSectionList) mergeTS(us, ue int64) {

	check := func(s, e int64) (hs, he, ts, te int64) {
		for _, t := range this.tss {
			// 分九种情况处理

			if s < t.s {

				if e > t.e {
					// t  |		s	e
					// one|	s			e
					t.overlap++
					return s, t.s, t.e, e
				} else if e > t.s && e < t.e {
					// t  |		s		e
					// one|	s		e
					this.tss = append(this.tss, &timeSection{s: e, e: t.e, overlap: t.overlap})
					t.overlap, t.e = t.overlap+1, e
					return s, t.s, 0, 0
				} else if e == t.e {
					// t  |		s	e
					// one|	s		e
					t.overlap++
					return s, t.s, 0, 0
				}
			} else if s > t.s {
				if e < t.e {
					// t  |	s			e
					// one|		s	e
					this.tss = append(this.tss, &timeSection{s: t.s, e: s, overlap: t.overlap})
					this.tss = append(this.tss, &timeSection{s: e, e: t.e, overlap: t.overlap})
					t.s, t.e, t.overlap = s, e, t.overlap+1
					return 0, 0, 0, 0
				} else if s < t.e && e > t.e {
					// t  |	s		e
					// one|		s		e
					this.tss = append(this.tss, &timeSection{s: t.s, e: s, overlap: t.overlap})
					t.overlap++
					t.s = s
					return 0, 0, t.e, e
				} else if e == t.e {
					// t  |	s		e
					// one|		s	e
					this.tss = append(this.tss, &timeSection{s: t.s, e: s, overlap: t.overlap})
					t.s = s
					t.overlap++
					return 0, 0, 0, 0
				}
			} else {
				// s == t.s
				if e > t.e {
					// t  | s	e
					// one|	s		e
					t.overlap++
					return 0, 0, t.e, e
				} else if e < t.e {
					// t  |	s		e
					// one|	s	e
					this.tss = append(this.tss, &timeSection{s: e, e: t.e, overlap: t.overlap})
					t.overlap++
					t.e = e
					return 0, 0, 0, 0
				} else if e == t.e {
					// t  |	s	e
					// one|	s	e
					t.overlap++
					return 0, 0, 0, 0
				}
			}
		}
		this.tss = append(this.tss, &timeSection{s: us, e: ue, overlap: 1})
		return 0, 0, 0, 0
	}

	hs, ht, ts, te := check(us, ue)
	if hs != 0 {
		this.mergeTS(hs, ht)
	}
	if ts != 0 {
		this.mergeTS(ts, te)
	}
}

func (this *timeSectionList)GetMaxDuration() timeSecion {
	var (
		max int
		s, e time.Time
	)
	for _, ts := range this.tss {
		s1 = time.Unix(ts.s, 0)
		e1 = time.Unix(ts.e, 0)
		if ts.overlap > max {
			max = ts.overlap
			s = s1
			e = e1
		} else if ts.overlap == max {
			// 重叠次数相同时,返回最长的时段
			if e1.Sub(s1) > e.Sub(s) {
				s = s1
				e = e1
			}
		}
	}
	return timeSecion{overlap:max, s:s, e:e}
}

除了性能不高外,实际的实现过程,需要考虑九种情况,包含各种起止时间重叠的情形。好在业务规模有限,也勉强撑着了。

优化思路

虽然这个功能不就就被下线了,但是心里有块石头始终放不下,实在是对N²复杂度不满意。

一开始考虑能否使用查找树来解决,但是由于时间段包含起始结束时间两个元素,所以这将会是一个定制的查找树,实现起来比暴力方法复杂,所以放弃了这个思路。

某日突然发觉,似乎栈非常适合用来解决这个问题。开始时间对应压栈,离开时间对应弹栈,栈内元素最多时,不就是想要的结果吗?

抽空整理了一下思路
拟定开始时间对应压栈动作、结束时间对应弹栈动作,则两类时间分别建立队列lstart、lend,并分别完成排序,创建栈结构,设当前最长时间段为stime-etime,同时在线人数为n,对应为栈的元素数量,还是以(9:15-9:20)(9:18-9:30)这两个时间段为例,有如下步骤:

  • 从lstart队列头取第一个时间9:15压栈,并从lstart移除,此时在线人数为1,则n=1,stime=9:15,etime=nil
  • 对比lstart与lend队列头元素,9:18<9:20,将9:18压栈,并从lstart移除,此时在线人数为2,比n大,则n=2,stime更新为9:18
  • 对比lstart与lend队列头元素,由于lstart为空,需要取lend,首先弹栈,在线人数变为1,比n小,n不变,stime也不变,etime更新为9:20,并从lend移除
  • 重复上一动作,lend被清空,此时n=2,stime=9:18,etime=9:20,至此算法结束
优化分析

综合来看,排序部分为NlogN的复杂度,出入栈操作为N,整体复杂度从暴力算法的N²下降到NlogN,实际项目应用中,起止时间保存在数据库中,如果库中对在时间列上有B+tree索引,则排序部分复杂度可以下降为N,那么整体的复杂度就是线性复杂度了。

排序部分代码就不写了,下面是栈操作代码

func MaxTimeDuration(start, end []time.Time) (int, time.Time, time.Time) {
	var (
		max int
		ts  time.Time
		te  time.Time
		// 这个stack是自己封装的,代码在https://github.com/bournex/basic_training/blob/master/structures/base/stack/stack.go
		stack = stack.MakeStack(100) 
	)

	closeTime := func(e time.Time) {
		l := stack.Length()
		v, _ := stack.Pop()
		s := v.(time.Time)

		if l > max {
			// 如果当前close的时间段重叠层级高于之前保存的,则更新ts,te
			max = l
			ts, te = s, e
		} else if l == max {
			// 如果当前close的时间段重叠层级等于当前保存的
			// 判断时间长短,选择时间较长的那个
			if !ts.IsZero() && !te.IsZero() {
				if te.Sub(ts) < e.Sub(s) {
					ts, te = s, e
				}
			} else {
				ts, te = s, e
			}
		}
	}

	for len(start) > 0 {
		s := start[0]
		e := end[0] // start不为空则end不可能为空

		if s.Before(e) {
			start = start[1:]
			stack.Push(s) // 压栈
		} else if s.After(e) {
			closeTime(e) // 弹栈
			end = end[1:]
		}
	}

	closeTime(end[0])

	return max, ts, te
}

需要留意得是,如果在线人数最多的时段有多个,还需要查找出最长的时段。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值