基于C#实现树状数组

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

一、概序

假如我现在有个需求,就是要频繁的求数组的前 n 项和,并且存在着数组中某些数字的频繁修改,那么我们该如何实现这样的需求?当然大家可以往真实项目上靠一靠。
**① 传统方法:**根据索引修改为 O(1),但是求前 n 项和为 O(n)。
**② 空间换时间方法:**我开一个数组 sum[],sum[i]=a[1]+…+a[i],那么有点意思,求 n 项和为 O(1),但是修改却成了 O(N),这是因为我的 Sum[i]中牵涉的数据太多了,那么问题来了,我能不能在相应的 sum[i]中只保存某些 a[i]的值呢?好吧,下面我们看张图。
image.png
从图中我们可以看到 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=01102 ;可以发现末尾连续的 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 函数

 #region 当前的sum数列的起始下标
 /// <summary>
 /// 当前的sum数列的起始下标
 /// </summary>
 /// <param name="i"></param>
 /// <returns></returns>
 public static int Lowbit(int i)
 {
     return i & -i;
 }
 #endregion

2、求前 n 项和

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

 #region 求前n项和
 /// <summary>
 /// 求前n项和
 /// </summary>
 /// <param name="x"></param>
 /// <returns></returns>
 public static int Sum(int x)
 {
     int ans = 0;

     var i = x;

     while (i > 0)
     {
         ans += sumArray[i - 1];

         //当前项的最大子树
         i -= Lowbit(i);
     }

     return ans;
 }
 #endregion

3、修改

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

 public static void Modify(int x, int newValue)
 {
     //拿出原数组的值
     var oldValue = arr[x];

     for (int i = x; i < arr.Length; i += Lowbit(i + 1))
     {
         //减去老值,换一个新值
         sumArray[i] = sumArray[i] - oldValue + newValue;
     }
 }

最后上总的代码:

 using System;
 using System.Collections.Generic;
 using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;
using System.IO;

namespace ConsoleApplication2
{
  public class Program
  {
      static int[] sumArray = new int[8];

      static int[] arr = new int[8];

      public static void Main()
      {
          Init();

          Console.WriteLine("A数组的值:{0}", string.Join(",", arr));
          Console.WriteLine("S数组的值:{0}", string.Join(",", sumArray));

          Console.WriteLine("修改A[1]的值为3");
          Modify(1, 3);

          Console.WriteLine("A数组的值:{0}", string.Join(",", arr));
          Console.WriteLine("S数组的值:{0}", string.Join(",", sumArray));

          Console.Read();
      }

      #region 初始化两个数组
      /// <summary>
      /// 初始化两个数组
      /// </summary>
      public static void Init()
      {
          for (int i = 1; i <= 8; i++)
          {
              arr[i - 1] = i;

              //设置其实坐标:i=1开始
              int start = (i - Lowbit(i));

              var sum = 0;

              while (start < i)
              {
                  sum += arr[start];

                  start++;
              }

              sumArray[i - 1] = sum;
          }
      }
      #endregion

      public static void Modify(int x, int newValue)
      {
          //拿出原数组的值
          var oldValue = arr[x];

          arr[x] = newValue;

          for (int i = x; i < arr.Length; i += Lowbit(i + 1))
          {
              //减去老值,换一个新值
              sumArray[i] = sumArray[i] - oldValue + newValue;
          }
      }

      #region 求前n项和
      /// <summary>
      /// 求前n项和
      /// </summary>
      /// <param name="x"></param>
      /// <returns></returns>
      public static int Sum(int x)
      {
          int ans = 0;

          var i = x;

          while (i > 0)
          {
              ans += sumArray[i - 1];

              //当前项的最大子树
              i -= Lowbit(i);
          }

          return ans;
      }
      #endregion

      #region 当前的sum数列的起始下标
      /// <summary>
      /// 当前的sum数列的起始下标
      /// </summary>
      /// <param name="i"></param>
      /// <returns></returns>
      public static int Lowbit(int i)
      {
          return i & -i;
      }
      #endregion
  }
}

image.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神仙别闹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值