手写线段树 第一场第一镜

静谧的夜最适合刷算法题了。刷着刷着发现了一个好玩的数据结构叫作线段树,听说是算法竞赛的常客哦,因而就本身写一个简单的玩玩。算法

原理

想了解线段树的同志们请移步,里边有原理和示意图: 百度百科,线段树数组

需求和痛点

我用它来主要是为了快速找到数组某区间内的数字和,而且在修改数组某几个元素以后再次找区间内的数字和。可想而知,我有两个需求:求和,修改。数据结构

正常状况下,由于咱们并不想给数组排序,那么,咱们能够用O(1)的复杂度进行修改,用O(n)的复杂度遍历区间来实现求和。可是,当咱们须要频繁进行求和的操做时,看起来很美的O(n)就变成了墙上的蚊子血,不再是当初的红玫瑰了,因而,咱们只能优化它。性能

我选择了线段树,典型的空间换时间。我用数组实现了二叉的线段树,用了4倍的额外空间。这颗二叉树的叶子节点是原始数组的各个元素,非叶子节点存储的为某段区间的和,一直到根节点逐步合并区间,根节点正好就是原始数组从头至尾的最大区间的元素和,具体的能够看看代码,我的以为代码比文字更直观哈哈。这样的处理将求和操做变成了O(logN),而相应的修改操做也增长到了O(logN),两个对数级别每每老是要好过一个常数的和一个线性的,不是么?优化

Talk is cheap. Show me the code.ui

首先是构建线段树,明白原理以后就像二叉树同样直接递归着搞就好啦:spa

type segmentTree struct {
	data []int //原始数组
	tree []int //线段树数组
}

//初始化线段树,至于为何须要4倍空间,只要我们理解了二叉树就一目了然了,线段树每一个节点存储的就是某一段的区间和
func NewSegmentTree(num []int) *segmentTree {
	countNum := len(num)
	data := make([]int, countNum)
	for k, v := range num {
		data[k] = v
	}
	tree := make([]int, 4*countNum)
	if countNum > 0 {
		var buildTree func(int, int, int) buildTree = func(index, left, right int) {
			if left == right {
				tree[index] = num[left]
				return
			}
			leftChild := leftChild(index)
			rightChild := rightChild(index)
			mid := left + ((right - left) >> 1)
			buildTree(leftChild, left, mid)
			buildTree(rightChild, mid+1, right)
			tree[index] = tree[leftChild] + tree[rightChild]
		}
		buildTree(0, 0, countNum-1)
	}
	return &segmentTree{data, tree}
}
复制代码

而后就是求和操做了,咱们把各个区间的和分别保存好了,本质上就变成了二叉树上找常数个节点,因此固然是O(logN)了:code

//求和操做,只须要经过递归来找到最近的区间和就好
func (st *segmentTree) SumRange(start, end int) int {
	var sum func(int, int, int, int, int) int sum = func(index, left, right, start, end int) int {
		if left == start && right == end {
			return st.tree[index]
		}
		leftChild := leftChild(index)
		rightChild := rightChild(index)
		mid := left + ((right - left) >> 1)
		if start >= mid+1 {
			return sum(rightChild, mid+1, right, start, end)
		} else if end <= mid {
			return sum(leftChild, left, mid, start, end)
		}
		return sum(leftChild, left, mid, start, mid) + sum(rightChild, mid+1, right, mid+1, end)
	}
	return sum(0, 0, len(st.data)-1, start, end)
}
复制代码

修改操做,好记性不如烂笔头,用笔画画就能够:cdn

//修改操做,递归找到叶子节点改掉索引对应的值,而后回溯的过程改掉包含索引的全部区间的和
func (st *segmentTree) Update(i int, value int) {
	countNum := len(st.data)
	if i >= len(st.data) {
		return
	}
	st.data[i] = value
	var up func(int, int, int) up = func(index, left, right int) {
		if left == right {
			st.tree[index] = value
			return
		}
		leftChild := leftChild(index)
		rightChild := rightChild(index)
		mid := left + ((right - left) >> 1)
		if i >= mid+1 {
			up(rightChild, mid+1, right)
		} else if i <= mid {
			up(leftChild, left, mid)
		}
		st.tree[index] = st.tree[leftChild] + st.tree[rightChild]
	}
	up(0, 0, countNum-1)
}

func leftChild(i int) int {
	return (i << 1) + 1
}
func rightChild(i int) int {
	return (i << 1) + 2
}
复制代码

新手的代码老是有很大的优化空间的,走过路过的大爷们要不吝赐教哦。排序

固然了,这仅仅只是一个简单的线段树,只能解决个人小需求。还有更多的方案,好比用链表来构建树确定要更灵活,并且线段树并不只仅用来求和,能够实现更多面向区间的操做,更好玩的是能够懒惰更新,每次修改不着急从新构建整棵树,能够一点一点地来,这样更加优化了性能。你们若是对线段树感兴趣的话,查资料去吧~o( ̄︶ ̄)o

算法梦想家,来跟我一块儿玩算法,玩音乐,聊聊文学创做,我们一块儿天马行空!

在这里插入图片描述
相关文章
相关标签/搜索