很久没有更新技术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;
            }
        }


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

        }

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

下面是演示器的截图:
一种环形视图堆栈

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

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

下面是演示器的完整代码:包括了VS2003和vs2005的两个解决方案的版本。
https://files.cnblogs.com/hoodlum1980/CircularStackView.rar

相关文章:

  • 2022-12-23
  • 2021-04-28
  • 2021-05-25
  • 2022-12-23
  • 2022-12-23
  • 2022-01-05
  • 2022-12-23
  • 2022-01-02
猜你喜欢
  • 2021-12-24
  • 2021-10-07
  • 2021-09-10
  • 2021-09-14
  • 2022-12-23
  • 2021-10-20
相关资源
相似解决方案