【小技巧】如何判斷樹形結構產生循環

在呈現層級數據爲一個樹形視圖(TreeView)的時候,常常會遇到一個問題,就是要判斷這些層級數據會不會形成循環,否則在構造樹形的時候會出現堆棧溢出(StackoverflowException)的錯誤。css

那麼如何判斷是否循環呢?尤爲在保存層級數據是經過父節點Id的遞歸方式來保存的狀況下(保存層級數據還有一種方式就是層級化的Id)。兩種保存方式都必需要求每一個節點數據都具備惟一的Id。html

以前本身寫過一種簡單的判斷方法,昨天又要從新實現相似算法(且數據結構不太同樣),就打算看看網絡上有沒有一種更好的方式。結果搜索後,竟然看到假設樹形層級的一個限值,若是超過限值就中止構造。這樣的解決辦法看着也是醉了。node

下面簡單介紹一下我用過的兩種解決辦法。算法

第一種最簡單的方式就是每次新增子節點都判斷要新增的節點的Id是否出如今當前節點的父節點鏈上。即檢查當前節點的父祖節點的Id是否包含打算要添加節點的Id(且一直檢查到根節點,檢查的具體算法可使用循環或者遞歸的方式)。可是這種方式要求構建數據模型的時候,必須包含一個Parent的屬性。(具體代碼就省略了……,也懶得翻以前的代碼)。(2014-11-25 23:52:46更新:仍是補充點代碼,見下)網絡

public interface IHasParent<T>
{
    T GetParent();
}

public interface IHasId<T>
{
    T GetId();
}

/// <summary>
/// 在本身和全部父輩層級中是否包含此Id(用於判斷是否循環)
/// </summary>
/// <typeparam name="TData">實現了IHasParent和IHasId的對象</typeparam>
/// <typeparam name="TId">Id的類型</typeparam>
/// <param name="self">當前對象</param>
/// <param name="id">需判斷的Id</param>
/// <returns></returns>
public static bool ExistInAncestry<TData, TId>(this TData self, TId id)
    where TData : class, IHasParent<TData>, IHasId<TId>
{
    var current = self;
    while (current != null)
    {
        if (Equals(current.GetId(), id)) return true;
        current = current.GetParent();
    }
    return false;
}

若是沒有Parent屬性的狀況下,又要如何判斷呢?就是昨天我使用的第二種方式:構造一個輔助字典集合來記錄全部樹形分支上的全部Id,而後判斷這些Id是否有重複。下面的代碼片斷基本能夠解釋這種用法:數據結構

public List<PartNode> GenerateTree()
{
    var nodes = new List<PartNode>();
    
    //建立循環判斷字典,Key是全部層級上的節點索引構造的字符串,Value是這個分支上全部的節點Id(本例爲Code)
    var cycleCheckerDict = new Dictionary<string, HashSet<string>>();
    cycleCheckerDict.Add("-1", new HashSet<string>(new[] { Parts[0].Code }));
    
    AddNodes(Parts[0], nodes, 1, cycleCheckerDict, "-1");
    return nodes;
}

private void AddNodes(PartNodeInfo parent, List<PartNode> nodes, double parentQuantity, Dictionary<string, HashSet<string>> cycleCheckerDict, string parentCycleCheckerKey)
{
    int nodeIndex = -1;
    
    var groupsByCode = from part in Parts
                       where part.ParentCode == parent.Code && !part.IsOutsourcingSubstitute
                       group part by part.Code;

    foreach (var group in groupsByCode)
    {
        var part = group.ToList()[0];
        var node = new PartNode
        {
            Amount = Setting.IsUnitAmount ? part.Quantity : group.Sum(o => o.Quantity) / parentQuantity,
            Code = part.Code,
            Description = part.Description,
            Name = part.Name,
            Specification = part.Specification,
            Unit = UnitMappings[part.Unit],
            Weight = part.Weight,
            WeightUnit = part.WeightUnit
        };
        nodes.Add(node);
        nodeIndex++;
 
        //判斷是否循環
        var parentCycleCheckerIdChain = cycleCheckerDict[parentCycleCheckerKey];
        if (parentCycleCheckerIdChain.Contains(part.Code))
        {
            ErrorLog.AppendLine(string.Format("零部件 {0}({1}) 會形成樹形循環",
                part.Name, part.Code));
            continue;
        }
        var currentCycleCheckerKey = parentCycleCheckerKey + "," + nodeIndex;
        var currentCycleCheckerIdChain = new HashSet<string>(parentCycleCheckerIdChain);
        currentCycleCheckerIdChain.Add(part.Code);
        cycleCheckerDict.Add(currentCycleCheckerKey, currentCycleCheckerIdChain);

        //添加下級零部件
        node.Nodes = new List<PartNode>();
        AddNodes(part, node.Nodes, correctQuantity, cycleCheckerDict, currentCycleCheckerKey);
    }

    cycleCheckerDict.Remove(parentCycleCheckerKey);//刪除遺留Id記錄鏈
}

固然,第一種方法要比第二種方法簡便(甚至效率也可能會更好,我沒有實際測試,也沒有計算算法複雜度),且第一種方法易於封裝爲通用的函數。app

最後留一個問題,如何在樹形上顯示循環的數據?函數

相關文章
相關標籤/搜索