|
楼主 |
发表于 2012-12-20 16:56
|
显示全部楼层
本帖最后由 lee1892 于 2012-12-24 16:42 编辑
使用队列和堆栈获得正确的顺序
1、队列加塞
在前文中使用队列来缓存后续的工作,由于受队列的限制,队列中层数高的总是在层数低的后面,导致获得的结果是按层级优先排列的,显然不是我们想要的结果。
一种解决方法是,在从队列中取得前端元素并写入结果数组时,采取插入到正确位置的办法,而不是写入到数组的末尾。
另一种办法则破坏了队列的规则,即在后续查询获得结果时不是在末端进入队列,而是插入队列。
下面的代码采用的是第二个方法。
[code=vb]
Private Sub GetChildren(sNo$, aRows, nCount&, iLayer&)
Dim i&, nIndex&, cQueue As New Collection, aInfo, nQueCount&
iLayer = 1
ReDim aInfo(1 To 3)
nIndex = 0 ' 用来保留循环初次查到结果时,其应插入的位置
cQueue.Add Item:=0, Key:="KEY" & nIndex
' 由于是插入添加至队列,所以初始化时先加一个进入队列,
' 可将其理解为第一层的父节点
Do
For i = 2 To UBound(aData)
If sNo = aData(i, nParentCol) Then
nQueCount = nQueCount + 1
' 队列产生过的元素计数变量,用于生成队列中元素的不重复编号
aInfo(1) = iLayer
aInfo(2) = i
aInfo(3) = nQueCount
' 信息记录数组多一项,用以记录队列元素编号
cQueue.Add Item:=aInfo, Key:="KEY" & nQueCount, After:="KEY" & nIndex
' 在队列中插入,在其父节点编号nIndex之后
nIndex = nQueCount
' 在第一个子节点后的项目应插入前一个兄弟节点之后
End If
Next
If cQueue.Count > 1 Then
cQueue.Remove 1 ' 由于初始化时添加过一个项目,所以先删
' 此后循环时,第一个总是上一次查找用的信息
aInfo = cQueue.Item(1)
iLayer = aInfo(1) + 1
sNo = aData(aInfo(2), nItemCol)
nIndex = aInfo(3) ' 下次查找时,以队列第一项的编号作为插入位置标记
nCount = nCount + 1
aRows(1, nCount) = aInfo(1)
aRows(2, nCount) = aInfo(2)
Else
Exit Do
End If
Loop
cQueue.Remove 1: Set cQueue = Nothing
' 队列內还剩有最后一个元素
End Sub
[/code]
2、堆栈倒查
前面的使用堆栈的代码获得的顺序虽然是按从属关系排列的,但同层中则是原顺序的倒序。同样也有两个方法解决。
一个是在写入结果数组中时总是在其前端写入。二则是在查询时反向查找,即把For语句改成:
For i = UBound(aData) To 2 Step -1
3、用堆栈模拟递归
我们知道递归的实现是编译器在内存中构建了堆栈寄存器保留了当前执行的结果,以使得当递归返回时能从前次执行的停止处开始接着执行。
那么我们也可以使用自构建的堆栈寄存器来达到这一目的。实际上,理解好下面这段代码,对理解递归是有很大帮助的。值得注意的是,下面这段代码中,iLayer这个参数完全没有使用,这是由于在堆栈中自始至终总是只保留了当前查询的各级上级节点,所以堆栈中的数量即是层数。为了帮助理解,可以将层数的计算代码加入,参考之前的递归代码,看看哪里是该+1哪该-1。
下面的代码直接在平板上写的,明儿看看对不~
- Private Sub GetChildren(sNo$, aRows, nCount&, iLayer&)
- Dim i&, cStack As New Collection, nLastTimeRow&, nStackCount&, nIndex&
- nLastTimeRow = 1 ' 初始值1
- Do
- For i = nLastTimeRow + 1 To UBound(aData)
- If sNo = aData(i, nParentCol) Then
- nCount = nCount + 1
- aRows(1, nCount) = cStack.Count + 1 ' 层数即为堆栈中的元素数量 +1
- aRows(2, nCount) = i
- ' 如同之前的递归代码,进寄存器时写入结果
- ' 而不是像前面的几个出寄存器时写入
- cStack.Add nCount ' 找到符合项,进入堆栈末端,只记录结果的编号
- Exit For
- End If
- Next
- If cStack.Count = 0 Then Exit Do ' 如果堆栈为空则退出查询
- nStackCount = cStack.Count
- nIndex = cStack.Item(nStackCount) ' 取得堆栈末端记录的结果数组编号
- If i <= UBound(aData) Then
- ' 如果 i 不大于原始数据数量,说明本次循环查到了符合项,需要查其子节点
- sNo = aData(aRows(2, nIndex), nItemCol)
- ' 子节点的所属 产品/部件 编号(GC列)是本节点的 部件/材料 编号(SGC列)
- nLastTimeRow = 1 ' 查子节点由第一项开始
- Else
- ' 否则说明本次查询结束,应返回查询兄弟节点
- sNo = aData(aRows(2, nIndex), nParentCol)
- ' 兄弟节点的所属 产品/部件 编号与本节点相同,即本节点的GC列
- nLastTimeRow = aRows(2, nIndex) ' 这个写错了,应该是2,用平板写的时候写成1了,55~~
- ' 查找兄弟节点时从本节点地址开始
- cStack.Remove cStack.Count
- ' 本节点的子节点查询结束,故将本节点从堆栈中删除
- End If
- Loop
- Set cStack = Nothing
- End Sub
复制代码
小结
至此,我们已经了解了数据结构中的基础知识,锻炼了迭代、递归的思维逻辑。经常看到论坛里有因为不知道循环次数(层的数量)而不知所措的,时常有人提出的方法是假设有足够大的层数之类的,相信通过前面的学习,我们已经有足够多的方法来应付了。
此后的代码,我们都将使用递归的方法,不要担心最开始提到的那个递归5000多次使得VBA堆栈空间溢出的问题,通常在我们处理的现实世界中的问题不太可能碰到这样极端的情况,即便有也可以通过数据结构和算法的优化避免。
=============================================
Original Post
用平板编辑贴子会出乱码,讨厌啊~
|
|