• 一种环形视图堆栈


    很久没有更新技术blog了,所以炒下从前的冷饭。这是一个我在过去的项目中设计的数据结构。在一个矢量图浏览器中,需要缓存视图状态(实际上是起始坐标和缩放比例等参数),当用户点击向前,向后视图时,能显示相应的视图。

    在过去,我们使用了一个一维的数组来缓存视图参数,但是当数组存满以后,所有缓存数据需要依次被推动前移,最前方的数据被丢弃。(溢出行为倒是有点像队列,只不过使用的时候它还是栈的特点,所以取名还是堆栈)。

    为了节约移动堆栈数据时的成本(开销为O(n)),所以想到把一维数组的首尾连接成为圆环,这样当数据已经满时,不需要移动所有里面的数据,只需要把新的数据覆盖掉失效数据即可,这样就可以把时间开销从O(n)降低为O(1)了。

    但是由于首尾连接成环形,但是本质上还是一个一维满栈的数据结构,所以必须设置两个指针指示栈的入口和底部的位置。此外由于视图可能回退中某个中间视图,因此还需要另一个指针,指示当前视图的位置。

    实际上定义的时候所有缓存是放在一个一维数组中。上面的三个“指针”实际是数组的索引(int32),分别用top,current,bottom表示。
        private MapView[] m_views;
        private int m_top;
        private int m_current;
        private int m_bottom;

    由于逻辑上是环形,我们定义以下按照环形展开成直线以后,向左右两个方向移动指针的方法,返回新的索引位置:
            /// <summary>
            
    /// 向右移动指针!
            
    /// </summary>
            
    /// <param name="p">指针当前位置</param>
            
    /// <returns>指针向右移动一格的位置</returns>
            public int MoveRight(int p)
            {
                
    int p2=p+1;
                
    if(p2>this.m_views.Length-1 || p2<0)
                    p2
    =0;
                
    return p2;
            }

    在新的数据压入堆栈时,使用下面的Push方法:
            public void Push(MapView mv)
            {
                
    //指针前进
                this.m_current=this.MoveRight(this.m_current);
                
    this.m_views[this.m_current]=mv;

                
    //判断当前位置是否“追上”了bottom(两个指针位置重叠)
                
    //注意在初始化时bottom指向0,并且第一次重合时,top为-1,bottom此时不移动
                
    //即刚刚入栈第一个数据时,三个指针位置重合,此后top和bottom永远不会重合
                if(this.m_current==this.m_bottom && this.m_top>=0)
                    
    this.m_bottom=this.MoveRight(this.m_bottom);

                
    //更新队列顶部的位置
                this.m_top=this.m_current;
            }

    当取出前一个视图时,相当于使用“向左后退到”Pop出一个数据,使用下面的GetPreviousView函数:
            /// <summary>
            
    /// 获取前一个视图数据(向堆栈的bottom方向获取)
            
    /// 【注意】弹出失败时,内部指针不可以移动!
            
    /// </summary>
            
    /// <param name="mv"></param>
            
    /// <returns></returns>
            public bool GetPreviousView(out MapView mv)
            {
                
    //设置输出值
                mv=this.m_views[this.m_current];
                
    //如果当前位置和bottom重合,则无法获取左侧视图
                
    //注意必须判断有没有视图入栈过,堆栈为空时,current指向-1
                if(this.m_current==this.m_bottom || this.m_current<0)
                    
    return false;
                
    else
                {
                    
    //弹出成功,因此更新指针位置
                    this.m_current=this.MoveLeft(this.m_current);
                    mv
    =this.m_views[this.m_current];
                    
    return true;
                }
            }

    这里注意和栈不同之处在于,pop出来的数据在数据结构中并没有被丢弃,而是还可以用“向右前进到”继续得到。所以还存在下面的一个GetNextView函数:
            public bool GetNextView(out MapView mv)
            {
                
    //设置out参数
                mv=this.m_views[this.m_current];
                
    //判断当前指针已经是顶部,则无法获取Next数据,当前指针也不向前移动
                
    //注意初始化时,current和top都指向-1
                if(this.m_current==this.m_top || mv.IsEmpty)
                    
    return false;
                
    else
                {
                    
    //获取成功,指针向前移动一格
                    this.m_current=this.MoveRight(this.m_current);
                    mv
    =this.m_views[this.m_current];
                    
    return true;
                }
            }


    以上是这个数据结构类的主要功能函数,但是实际上上面的函数的技巧性不大。我在这个类中写的最难,灵活性最大的一个方法是重设堆栈长度,也就是下面的这个方法,实际上我是花了很多时间和调试才写正确的。当然如果过去的数据全都舍弃,那么这里也就不存在什么问题了。而一旦我想要把现有数据放到新申请的新的长度的数组中时,要花费很多功夫注意复制的细节:所以重设长度时,我把它写成一个方法,而非属性。这样要考虑到的问题是,如果新的长度比现在的长度大,则现在有的数据全部可以有效复制过去,如果小,那么必然要舍弃一小部分。而且要维持现有的几个指针位置正确。

            /// <summary>
            
    /// 重新设置新的堆栈深度
            
    /// (这个方法有些复杂,因此写成方法而不是属性)
            
    /// </summary>
            
    /// <param name="length"></param>

            public void SetLength(int length)
            
    {
                
    //如果参数和当前深度相等,或者不合法,则直接返回
                if(this.m_views.Length==length || length<=0)
                    
    return;
                
    //判断是否处于空栈状态(未进入数据)
                if(this.m_current<0)
                
    {
                    
    this.m_views=new MapView[length];
                    
    return;
                }


                
    //先获取以当前位置为分界的previous和next个数
                int preCount=this.PreviousViewsCount;
                
    int nextCount=this.NextViewsCount;

                
    //临时数据
                MapView view;
                
    //创建一个新长度的temp堆栈
                MapView[] temp=new MapView[length];

                
    //因为弹出视图时会变动current指针位置,因此先缓存current指针
                int cache_current=this.m_current;
                
    //拷贝当前视图到索引0的位置
                temp[0]=this.m_views[this.m_current];
                
    //拷贝【Next】视图
                int count1=Math.Min(length-1,nextCount);    //需要拷贝的next视图个数
                for(int i=1;i<=count1;i++)
                
    {
                    
    if(this.GetNextView(out view))
                        temp[i]
    =view;
                }

                

                
    //如果还有剩余位置,把【Previous元素】拷贝到当前队列的尾部!
                int count2=Math.Min(length-1-count1,preCount);
                
    //弹出视图之前,恢复current指针的位置!!!
                this.m_current=cache_current;
                
    for(int i=0;i<count2;i++)
                
    {
                    
    if(this.GetPreviousView(out view))
                        temp[length
    -1-i]=view;
                }


                
    //更新top指针(这里count1肯定大于1)
                this.m_top = (count1>0)? count1 : 0;
                
    //更新bottom位置
                this.m_bottom= count2>0? length-count2:0;
                
    //更新当前指针位置
                this.m_current=0;
                
    //更新堆栈为新建堆栈
                this.m_views=temp;
            }

    上面的方法最开始没有写对,因为也没有开始做后来的视图堆栈演示器,所以有的错误无法发现,直到后来写了演示器,才发现里面的问题,于是改正。
    此外这个类还提供的属性有前视图个数,后视图个数,是否满等。

    下面是演示器的截图:


    在图中是一个用户定义控件,外围的蓝色箭头T表示top,红色的箭头B表示bottom,蓝色块表示current,内围的一个淡蓝色弧形线段表示的是可展开成直线的有效数据(圆头方向表示栈的顶部),现在的堆栈长度为16。

    可以改变长度,可以看到改变长度以后的图形:下图中将深度重新设置为8以后,新的指针位置和数据。


    下面是演示器的完整代码:包括了VS2003和vs2005的两个解决方案的版本。
    https://files.cnblogs.com/hoodlum1980/CircularStackView.rar
  • 相关阅读:
    js事件分类
    过3s弹出广告条,点叉号关闭
    js进阶
    js入门
    html
    R Markdown + Infinite Moon Reader + 编辑实时更新
    png转ico+windows图标+GIMP
    微生物+计算细胞倍增时间
    使用Mathjax网页插入公式
    Firefox+zoom+全局缩放比例
  • 原文地址:https://www.cnblogs.com/hoodlum1980/p/954818.html
Copyright © 2020-2023  润新知