c++ 数据结构之 线段树

        线段树是一种数据结构,是一种二叉树。线段树将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。线段树对于区间求和等区间操作能够实现复杂度为O(logn)的操作,故得以广泛利用。修改一个值的操作也是O(logn)。

        线段树是建立在线段基础上,每个结点都代表了一条线段[a,b]。长度为1的线段称为元线段。非元线段都有两个子结点,左结点代表的线段为[a,(a + b) / 2],右结点代表的线段为[((a + b) / 2)+1,b]。

线段树示意图如下:

u=1091946519,1765257349&fm=26&gp=0.png

        在上图中,叶子结点(绿色)是真正的节点,而上面蓝色的线段则表示一段线段的和。(这里面线段树就以区间加和为例)。比如说A[0...9]实际上就是从索引0加和到9得到的结果。所以说,任何一个节点的值都等于其左右两个子节点的和。

        线段树的求和和更新用到的知识的递归和分治。比如说更新一个索引为i的值为val。那么要找到这个点然后分别更新它的父节点,父节点的父节点等等。

        我们在这里规定根节点的索引为0,那么定位某个索引为index根节点的左孩子右孩子,就类似堆的数据结构,左孩子索引为2*index+1,2*index+2。

1. 当创建一个线段树的时候要从下至上创建根节点,两个孩子创建好以后,根据需要的业务逻辑(区间和,区间最大值,区间最小值),来创建这两个孩子的根节点。
2. 当查询一段区间和的时候,查询区间有可能在线段树某个节点的左半区间(左孩子)那就递归查询左孩子,或者右半区间(右孩子)那就递归查询右孩子,或者(左半右半都有一部分),就递归查询左半部分,右半部分,然后根据业务逻辑合并返回
3. 当更新一个节点值的时候:如果更新位置在节点的左半区间(左孩子),就递归更新左孩子,如果在右半区间,就递归更新右孩子。更新完成以后,里面的小区间变化了,包裹的大区间即该节点也要根据业务逻辑更新

下面我们看一下用数组实现线段树以及更新单点的值,以及求区间的值是如何实现的(注释写的非常详细了):

//线段树
class SegmentTree{
public:
	vector<int> tree; //存放线段树的数字
	int lentree;
	SegmentTree(vector<int> &nums){//用nums来初始化tree数组
		
		lentree = nums.size(); //记录树中叶节点的长度
		
		/*初始化4N长度的线段树,默认值是0。 索引从0开始,一个节点i的子节点索引分别是2i+1和2i+2*/
		for (int i = 0; i < 4 * lentree; i++) tree.push_back(0);
		
		buildTree(nums, 0, 0, lentree - 1);//建树函数
	}
	~SegmentTree(){}

	//tree_index 是节点在线段树的索引。l和r是线段树中任何一个节点的左边界和右边界,当l==r时候表示单个节点
	void buildTree(vector<int> &nums, int tree_index, int l, int r){
		//如果是叶子结点
		if (l > r) return;
		if (l == r){
			tree[tree_index] = nums[l];
			return;
		}
		int mid = (l + r) / 2;//将nums数组分成【l, mid】和【mid + 1, r】两段
		//计算左右子节点的索引
		int left = 2 * tree_index + 1;
		int right = 2 * tree_index + 2;

		//分别构建左右子树
		buildTree(nums, left, l, mid);
		buildTree(nums, right, mid + 1, r);
		
		//这里是用了分治法的思想,建好了左右两个子树然后再计算两者之和,得到整个区间的和
		tree[tree_index] = tree[left] + tree[right];

	}

	//查询的是ql到qr之间的和。l,r表示当前查询的左右边界。tree_index表示当前查询到的节点的编号
	int query(int tree_index, int l, int r, int ql, int qr){
		
		if ((l == ql) && (r = qr)){//已经查询到叶子结点了
			return tree[tree_index];
		}
		int mid = (l + r) / 2;
		int left = 2 * tree_index + 1;
		int right = 2 * tree_index + 2;
		if (qr <= mid){//要查询的部分完全在线段树的左半段
			return query(left, l, mid, ql, qr);
		}
		else if (ql > mid){//要查询的部分完全在线段树的右半段
			return query(right, mid + 1, r, ql, qr);
		}
		else{//左右两段都有
			return query(left, l, mid, ql, mid) + query(right, mid + 1, r, mid + 1, qr);
		}
	}

	void update(int tree_index, int l, int r, int index, int val){//更新位于某个叶子结点(索引为index)的值为val
		if (l == r){
			tree[tree_index] = val;
			return;
		}
		int mid = (l + r) / 2;
		int left = 2 * tree_index + 1;
		int right = 2 * tree_index + 2;
		
		if (index <= mid){//索引index在mid左边,说明在左子树上面
			update(left, l, mid, index, val);
		}
		else{//在右子树上面
			update(right, mid + 1, r, index, val);
		}
		//同样用了分治的办法,左右子节点更新完毕之后再更新自己
		tree[tree_index] = tree[left] + tree[right];

	}
	//输出所有叶子结点的值
	void print(int tree_index, int l, int r){
		if (l > r) return;
		if (l == r){
			cout << tree[tree_index] << ' ';
			return;
		}
		int mid = (l + r) / 2;
		int left = 2 * tree_index + 1;
		int right = 2 * tree_index + 2;
		print(left, l, mid);
		print(right, mid + 1, r); 
	}
};


//外部调用线段树解决问题的类
class NumArray {
public:
	SegmentTree *segtree;
	NumArray(vector<int>& nums) {
		segtree = new SegmentTree(nums);
		segtree->print(0, 0, nums.size() - 1);
		cout << endl;
	}

	void update(int i, int val) {//外部调用线段树的更新值函数
		cout << "before update: " << endl;
		segtree->print(0, 0, segtree->lentree - 1);
		cout << "-----------------------------------" << endl;
		segtree->update(0, 0, segtree->lentree - 1, i, val);
		cout << "after update:" << endl;
		segtree->print(0, 0, segtree->lentree - 1);
		cout << "-----------------------------------" << endl;
	}

	int sumRange(int i, int j) {//区间求和
		return segtree->query(0, 0, segtree->lentree - 1, i, j);
	}
};

int main(){
	vector<int> nums = {1, 3, 5};
	NumArray* obj = new NumArray(nums);
	//修改索引1的值为2
	obj->update(1, 2);
	int param_2 = obj->sumRange(0, 2);
	cout << param_2 << endl;
	system("pause");
	return 0;
}

输出是:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值