经典算法题每日演练——第十题 树状数组

原文:经典算法题每日演练——第十题 树状数组

有一种数据结构是神奇的,神秘的,它展现了位运算与数组结合的神奇魅力,太牛逼的,它就是树状数组,这种数据结构不是神人是发现不了的。

一:概序

假如我现在有个需求,就是要频繁的求数组的前n项和,并且存在着数组中某些数字的频繁修改,那么我们该如何实现这样的需求?当然大家可以往

真实项目上靠一靠。

① 传统方法:根据索引修改为O(1),但是求前n项和为O(n)。

②空间换时间方法:我开一个数组sum[],sum[i]=a[1]+....+a[i],那么有点意思,求n项和为O(1),但是修改却成了O(N),这是因为我的Sum[i]中牵

涉的数据太多了,那么问题来了,我能不能在相应的sum[i]中只保存某些a[i]的值呢?好吧,下面我们看张图。

从图中我们可以看到S[]的分布变成了一颗树,有意思吧,下面我们看看S[i]中到底存放着哪些a[i]的值。

S[1]=a[1];

S[2]=a[1]+a[2];

S[3]=a[3];

S[4]=a[1]+a[2]+a[3]+a[4];

S[5]=a[5];

S[6]=a[5]+a[6];

S[7]=a[7];

S[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8];

之所以采用这样的分布方式,是因为我们使用的是这样的一个公式:S[i]=a[i-2k+1]+....+a[i]。

其中:2k 中的k表示当前S[i]在树中的层数,它的值就是i的二进制中末尾连续0的个数,2k也就是表示S[i]中包含了哪些a[],

举个例子:  i=610=0110;可以发现末尾连续的0有一个,即k=1,则说明S[6]是在树中的第二层,并且S[6]中有21项,随后我们求出了起始项:

a[6-21+1]=a[5],但是在编码中求出k的值还是有点麻烦的,所以我们采用更灵巧的Lowbit技术,即:2k=i&-i 。

则:S[6]=a[6-21+1]=a[6-(6&-6)+1]=a[5]+a[6]。

二:代码

1:神奇的Lowbit函数

 1 #region 当前的sum数列的起始下标
 2         /// <summary>
 3         /// 当前的sum数列的起始下标
 4         /// </summary>
 5         /// <param name="i"></param>
 6         /// <returns></returns>
 7         public static int Lowbit(int i)
 8         {
 9             return i & -i;
10         }
11         #endregion

2:求前n项和

比如上图中,如何求Sum(6),很显然Sum(6)=S4+S6,那么如何寻找S4呢?即找到6以前的所有最大子树,很显然这个求和的复杂度为logN。

 1         #region 求前n项和
 2         /// <summary>
 3         /// 求前n项和
 4         /// </summary>
 5         /// <param name="x"></param>
 6         /// <returns></returns>
 7         public static int Sum(int x)
 8         {
 9             int ans = 0;
10
11             var i = x;
12
13             while (i > 0)
14             {
15                 ans += sumArray[i - 1];
16
17                 //当前项的最大子树
18                 i -= Lowbit(i);
19             }
20
21             return ans;
22         }
23         #endregion

3:修改

如上图中,如果我修改了a[5]的值,那么包含a[5]的S[5],S[6],S[8]的区间值都需要同步修改,我们看到只要沿着S[5]一直回溯到根即可,

同样它的时间复杂度也为logN。

 1         public static void Modify(int x, int newValue)
 2         {
 3             //拿出原数组的值
 4             var oldValue = arr[x];
 5
 6             for (int i = x; i < arr.Length; i += Lowbit(i + 1))
 7             {
 8                 //减去老值,换一个新值
 9                 sumArray[i] = sumArray[i] - oldValue + newValue;
10             }
11         }

最后上总的代码:

  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Diagnostics;
  6 using System.Threading;
  7 using System.IO;
  8
  9 namespace ConsoleApplication2
 10 {
 11     public class Program
 12     {
 13         static int[] sumArray = new int[8];
 14
 15         static int[] arr = new int[8];
 16
 17         public static void Main()
 18         {
 19             Init();
 20
 21             Console.WriteLine("A数组的值:{0}", string.Join(",", arr));
 22             Console.WriteLine("S数组的值:{0}", string.Join(",", sumArray));
 23
 24             Console.WriteLine("修改A[1]的值为3");
 25             Modify(1, 3);
 26
 27             Console.WriteLine("A数组的值:{0}", string.Join(",", arr));
 28             Console.WriteLine("S数组的值:{0}", string.Join(",", sumArray));
 29
 30             Console.Read();
 31         }
 32
 33         #region 初始化两个数组
 34         /// <summary>
 35         /// 初始化两个数组
 36         /// </summary>
 37         public static void Init()
 38         {
 39             for (int i = 1; i <= 8; i++)
 40             {
 41                 arr[i - 1] = i;
 42
 43                 //设置其实坐标:i=1开始
 44                 int start = (i - Lowbit(i));
 45
 46                 var sum = 0;
 47
 48                 while (start < i)
 49                 {
 50                     sum += arr[start];
 51
 52                     start++;
 53                 }
 54
 55                 sumArray[i - 1] = sum;
 56             }
 57         }
 58         #endregion
 59
 60         public static void Modify(int x, int newValue)
 61         {
 62             //拿出原数组的值
 63             var oldValue = arr[x];
 64
 65             arr[x] = newValue;
 66
 67             for (int i = x; i < arr.Length; i += Lowbit(i + 1))
 68             {
 69                 //减去老值,换一个新值
 70                 sumArray[i] = sumArray[i] - oldValue + newValue;
 71             }
 72         }
 73
 74         #region 求前n项和
 75         /// <summary>
 76         /// 求前n项和
 77         /// </summary>
 78         /// <param name="x"></param>
 79         /// <returns></returns>
 80         public static int Sum(int x)
 81         {
 82             int ans = 0;
 83
 84             var i = x;
 85
 86             while (i > 0)
 87             {
 88                 ans += sumArray[i - 1];
 89
 90                 //当前项的最大子树
 91                 i -= Lowbit(i);
 92             }
 93
 94             return ans;
 95         }
 96         #endregion
 97
 98         #region 当前的sum数列的起始下标
 99         /// <summary>
100         /// 当前的sum数列的起始下标
101         /// </summary>
102         /// <param name="i"></param>
103         /// <returns></returns>
104         public static int Lowbit(int i)
105         {
106             return i & -i;
107         }
108         #endregion
109     }
110 }

时间: 2024-10-14 17:30:49

经典算法题每日演练——第十题 树状数组的相关文章

经典算法题每日演练——第二十题 三元组

原文:经典算法题每日演练--第二十题 三元组 我们知道矩阵是一个非常强大的数据结构,在动态规划以及各种图论算法上都有广泛的应用,当然矩阵有着不足的地方就是空间和时间 复杂度都维持在N2上,比如1w个数字建立一个矩阵,在内存中会占用1w*1w=1亿的类型空间,这时就会遇到outofmemory...那么面 临的一个问题就是如何来压缩矩阵,当然压缩的方式有很多种,这里就介绍一个顺序表的压缩方式:三元组. 一:三元组 有时候我们的矩阵中只有零星的一些非零元素,其余的都是零元素,那么我们称之为稀疏矩阵,

经典算法题每日演练——第十六题 Kruskal算法

原文:经典算法题每日演练--第十六题 Kruskal算法 这篇我们看看第二种生成树的Kruskal算法,这个算法的魅力在于我们可以打一下算法和数据结构的组合拳,很有意思的. 一:思想 若存在M={0,1,2,3,4,5}这样6个节点,我们知道Prim算法构建生成树是从”顶点”这个角度来思考的,然后采用“贪心思想” 来一步步扩大化,最后形成整体最优解,而Kruskal算法有点意思,它是站在”边“这个角度在思考的,首先我有两个集合. 1. 顶点集合(vertexs): 比如M集合中的每个元素都可以认

经典算法题每日演练——第二十四题 梳排序

原文:经典算法题每日演练--第二十四题 梳排序 这篇再看看一个经典的排序,梳排序,为什么取名为梳,可能每个梳都有自己的gap吧,大梳子gap大一点,小梳子gap小一点. 上一篇我们看到鸡尾酒排序是在冒泡排序上做了一些优化,将单向的比较变成了双向,同样这里的梳排序也是在冒泡排序上做了一些优化. 冒泡排序上我们的选择是相邻的两个数做比较,就是他们的gap为1,其实梳排序提出了不同的观点,如果将这里的gap设置为一定的大小, 效率反而必gap=1要高效的多. 下面我们看看具体思想,梳排序有这样一个1.

经典算法题每日演练——第二十二题 奇偶排序

原文:经典算法题每日演练--第二十二题 奇偶排序 这个专题因为各种原因好久没有继续下去了,MM吧...你懂的,嘿嘿,不过还得继续写下去,好长时间不写,有些东西有点生疏了, 这篇就从简单一点的一个“奇偶排序”说起吧,不过这个排序还是蛮有意思的,严格来说复杂度是O(N2),不过在多核的情况下,可以做到 N2 /(m/2)的效率,这里的m就是待排序的个数,当m=100,复杂度为N2 /50,还行把,比冒泡要好点,因为重点是解决问题的奇思妙想. 下面我们看看这个算法是怎么描述的,既然是奇偶,肯定跟位数有

经典算法题每日演练——第二十五题 块状链表

原文:经典算法题每日演练--第二十五题 块状链表 在数据结构的世界里,我们会认识各种各样的数据结构,每一种数据结构都能解决相应领域的问题,每一种数据结构都像 是降龙十八掌中的某一掌,掌掌毙命... 当然每个数据结构,有他的优点,必然就有它的缺点,那么如何创造一种数据结构 来将某两种数据结构进行扬长避短,那就非常完美了.这样的数据结构也有很多,比如:双端队列,还有就是今天讲的 块状链表, 我们都知道 数组 具有 O(1)的查询时间,O(N)的删除,O(N)的插入... 链表 具有 O(N)的查询时

经典算法题每日演练——第十五题 并查集

原文:经典算法题每日演练--第十五题 并查集 这一篇我们看看经典又神奇的并查集,顾名思义就是并起来查,可用于处理一些不相交集合的秒杀. 一:场景 有时候我们会遇到这样的场景,比如:M={1,4,6,8},N={2,4,5,7},我的需求就是判断{1,2}是否属于同一个集合,当然实现方法 有很多,一般情况下,普通青年会做出O(MN)的复杂度,那么有没有更轻量级的复杂度呢?嘿嘿,并查集就是用来解决这个问题的. 二:操作 从名字可以出来,并查集其实只有两种操作,并(Union)和查(Find),并查集

经典算法题每日演练——第十二题 线段树

原文:经典算法题每日演练--第十二题 线段树 这一篇我们来看树状数组的加强版线段树,树状数组能玩的线段树一样可以玩,而且能玩的更好,他们在区间求和,最大,平均 等经典的RMQ问题上有着对数时间的优越表现. 一:线段树 线段树又称"区间树”,在每个节点上保存一个区间,当然区间的划分采用折半的思想,叶子节点只保存一个值,也叫单元节点,所 以最终的构造就是一个平衡的二叉树,拥有CURD的O(lgN)的时间. 从图中我们可以清楚的看到[0-10]被划分成线段的在树中的分布情况,针对区间[0-N],最多有

经典算法题每日演练——第十九题 双端队列

原文:经典算法题每日演练--第十九题 双端队列 话说大学的时候老师说妹子比工作重要~,工作可以再换,妹子这个...所以...这两个月也就一直忙着Fall in love,嗨,慢慢调整心态吧, 这篇就选一个简单的数据结构聊一聊,话说有很多数据结构都在玩组合拳,比如说:块状链表,块状数组,当然还有本篇的双端队列,是的,它就是 栈和队列的组合体. 一:概念 我们知道普通队列是限制级的一端进,另一端出的FIFO形式,栈是一端进出的LIFO形式,而双端队列就没有这样的限制级,也就是我们可以在 队列两端进行

经典算法题每日演练——第十八题 外排序

原文:经典算法题每日演练--第十八题 外排序 说到排序,大家第一反应基本上是内排序,是的,算法嘛,玩的就是内存,然而内存是有限制的,总有装不下的那一天,此时就可以来玩玩 外排序,当然在我看来,外排序考验的是一个程序员的架构能力,而不仅仅局限于排序这个层次. 一:N路归并排序 1.概序 我们知道算法中有一种叫做分治思想,一个大问题我们可以采取分而治之,各个突破,当子问题解决了,大问题也就KO了,还有一点我们知道 内排序的归并排序是采用二路归并的,因为分治后有LogN层,每层两路归并需要N的时候,最