'循环链表构建器'的最快算法是什么?

5

我有两个问题:

1)将此列表按“连接”顺序排序的最快算法是什么?
2)这是一个现有的问题/算法,它有什么名称?

节点始终以循环方式连接,因此我的起始ID无关紧要。

给定此列表:

id node1 node2
A  4     9
B  6     1
C  1     3
D  3    10
E  7     2
F  4     2
G  9     8
H 10     5
I  7     5
J  6     8

节点 node1 和 node2 没有特定的顺序,因此 id A 可以是 4 - 9 或者 9 - 4。

输出应该是这样的(以 A 开头并不重要,只要输出为一条链即可)。

node ids: 4 - 9 - 8 - 6 - 1 - 3 - 10 - 5 - 7 - 2 - 4
ids:        A   G   J   B   C   D    H   I   E   F

(我用C#编写我的代码。但是任何语言的伪代码都可以。)

列表结构已经定义为(id,node1,node2)的数组了吗?还是要定义列表结构以获得最佳解决方案? - lemon
你正在寻找类似于这个解决方案的东西。我认为你不可能得到一个性能更好的算法。 - InBetween
@lemon:它目前在数据库中,所以我可以将其读入任何我想要的地方。 - frankhommers
@InBetween:不完全是这样。那个解决方案有一个父/子关系。我的数据节点node1/node2可以是父节点或子节点。 - frankhommers
5个回答

2

这与此相关,但我的输入只连接了一个节点到两个节点,而不是更多。 - frankhommers
那么你的意思是,总会有一个解决方案?这意味着你只需要模拟单向循环来构建你的答案。 - algojava
是的,假设总会有解决方案,创建循环的最有效算法。 - frankhommers

1
我不知道这是否是某个命名的数学问题。以下是伪代码,可以以O(N)的方式(复杂度和内存使用)解决问题。
1)创建数组(我们假设节点具有唯一的ID范围为[0..N-1]。并将其填充到节点中(ID节点应放置在ID位置)。
2)选择任何节点并将其推入单独的列表中(它将包含“循环”顺序的节点)。该列表中的最后一个节点将被命名为已处理节点。
3)从1到N-1进行迭代,在每个步骤中选择未遍历的已处理节点的邻居。将这样的未遍历节点推入循环列表中。继续这个过程。
注意:检查“未遍历”属性可以以O(1)的方式执行。只需查看它是否已经存在于循环列表中。它应该是上一个(已处理)节点的邻居。
主要缺点 - 这种算法的假设是存在唯一的欧拉路径。

这是一种方法,但这需要多次迭代列表。我希望有更高效的方法。 - frankhommers

1
起始点是一个类似于列表或数组的东西,如下所示:
 1  {A, 4, 9}  
 2  {B, 6, 1}  
 3  {C, 1, 3}  
 4  {D, 3,10}  
 5  {E, 7, 2}  
 6  {F, 4, 2}  
 7  {G, 9, 8}  
 8  {H,10, 5}  
 9  {I, 7, 5}  
10  {J, 6, 8}  

如果我们可以将它改成像这样的列表或数组:
 1  {C, 1, 3}  
 2  {F, 2, 4}  (nodes swapped)
 3  {D, 3,10}  
 4  {A, 4, 9}  
 5  {I, 5, 7}  (nodes swapped)
 6  {B, 6, 1}  
 7  {E, 7, 2}  
 8  {J, 8, 6}  (nodes swapped)
 9  {G, 9, 8}  
10  {H,10, 5}  

如果按照节点1的顺序排序,那么我们可以将这个列表或数组读作链表:

start with item 1:  {C, 1, 3}
read node2: 3
skip to item 3:     {D, 3,10}
read node2: 10
skip to item 10:    {H,10, 5}
...
skip to item 6:     {B, 6, 1}
read node2: 1
end of list

result: C D H I E F A G J B

创建这个列表的第二个版本可以在原地完成,通过交换列表中的项目或将项目复制到新列表中(如果您有足够的空间)。唯一需要注意的是,有时您可能需要交换两个节点。在原地重新排序时,可能会像这样进行:
look at item 1:     {A, 4, 9}  
  if item 4 has a node1 different from 4, swap item 1 and 4  
  else, change item 1 to {A, 9, 4} and swap item 1 and 9
  (-> item 1 and 4 swapped)    

while current item is already in-place, skip to next
(-> stay at item 1)

look at new item 1: {D, 3,10}  
  if item 3 has a node1 different from 3, swap item 1 and 3  
  else, change item 1 to {D,10, 3} and swap item 1 and 10
  (-> item 1 and 3 swapped)

while current item is already in-place, skip to next
(-> item 1 is now {C, 1, 3}, so skip to item 2)

...

创建新的列表或数组时,这应该更加简单:
look at item 1:     {A, 4, 9}  
  if new list has no item 4, copy item 1 as item 4 to the new list
  else, change item 1 to {A, 9, 4} and copy as item 9 to the new list
move to next item

正如您所看到的,无需多次迭代列表;每个项目只交换或复制一次,并且其目的地由其node1或node2确定。

更新

我刚意识到,订购物品的步骤可能会比上述描述的(大大)增加。例如,如果您首先将 {A,4,8} 移动到位置 4,将 {B,5,9} 移动到位置 5,然后遇到 {C,4,5},您就会卡住。然后,您必须将 {C,4,5} 与其他两个项目之一交换,交换另一个项目中的节点,并将其移动到位。该新位置本身可能已被占用,等等,因此无法知道两个选项中哪个更小。在最坏的情况下,交换次数接近 N2


更新2

当然,可以通过将项目存储为双向链表来解决上述问题。例如,遇到{A,4,8}时,将{A,8}存储为项目4,将{A,4}存储为项目8,然后对于{B,5,9},将{B,9}存储为项目5,将{B,5}存储为项目9,然后对于{C,4,5},将其添加到已存储的项目中,使项目4变为{A,8,C,5},项目5变为{B,9,C,4}。然后可以在两个方向上遍历双向链表。这将增加需要完成的工作和使用的空间,但仍然与列表中的项目数量成线性关系。


有趣的想法。我不确定总是会有一个node1+1,这意味着我的id比示例更“随机”。有没有办法让这个工作? - frankhommers
如果id空间不太大,您可以使用稀疏数组;假设id是16位整数,则需要为65,536个项目分配空间,无论实际有多少项目。如果没有足够的空间,也许可以将id哈希成较小的空间? - m69 ''snarky and unwelcoming''
@frankhommers 我刚意识到我的建议在你的示例输入中运行良好,但不能保证对所有输入都有效(请参见更新)。 - m69 ''snarky and unwelcoming''
@frankhommers 我为更新中提到的问题添加了一个解决方法。 - m69 ''snarky and unwelcoming''

1
这里提出了一种使用字典(哈希表)进行计算的方案。
我把问题中提供的一行数据称为“单元格”(但我们不知道您的数据结构)。
由于字典提供O(1)的检索,所以似乎是O(n)的。
所有这些都假定初始数据与问题一致(至少在我理解的情况下)。
代码是用C#编写的,并进行了注释。如果注释不够详细,请告诉我。
class Program
{
    class Cell
    {
        public string Id { get; set; }
        public int Node1 { get; set; }
        public int Node2 { get; set; }

        public int Min { get { return Math.Min( Node1, Node2 ); } }

        public Cell( string name, int node1, int node2 )
        {
            Id = name;
            Node1 = node1;
            Node2 = node2;
        }

        public override string ToString()
        {
            return Id + "(" + Node1.ToString() + "," + Node2.ToString() + ")";
        }
    }

    static void Main( string[] args )
    {
        var A = new Cell( "A", 4, 9 );
        var B = new Cell( "B", 6, 1 );
        var C = new Cell( "C", 1, 3 );
        var D = new Cell( "D", 3, 10 );
        var E = new Cell( "E", 7, 2 );
        var F = new Cell( "F", 4, 2 );
        var G = new Cell( "G", 9, 8 );
        var H = new Cell( "H", 10, 5 );
        var I = new Cell( "I", 7, 5 );
        var J = new Cell( "J", 6, 8 );

        var cells = new List<Cell>() { A, B, C, D, E, F, G, H, I, J };

        // A dictionary to store the cells corresponding to each node values
        Dictionary<int, List<Cell>> dict = new Dictionary<int, List<Cell>>();

        // Add all the cells to the dictionary
        foreach ( var cell in cells )
            AddCell( dict, cell );

        // Start with arbitrary first cell and remove it from the dictionary
        var currentCell = GetCell( dict, A.Node1 );
        RemoveCell( dict, currentCell );

        // The result is a list of cells initialized with the first cell
        var result = new List<Cell>() { currentCell };

        // Calculation loop
        bool doContinue = true;
        while ( doContinue )
        {
            // Tries to get a next cell from the node1 of current cell...
            var nextCell = GetCell( dict, currentCell.Node1 );

            // ... or if not found, tries to get it from node2
            if ( nextCell == null )
                nextCell = GetCell( dict, currentCell.Node2 );

            if ( nextCell == null )
            // If not found, we stop
            {
                doContinue = false;
            }
            else
            // If found
            {
                // Add the next cell to the result
                result.Add( nextCell );
                // Remove the cell
                RemoveCell( dict, nextCell );
            }

            // The next cell becomes the current cell
            currentCell = nextCell;
        }

    }

    // Adding a cell puts the cell against its two nodes values
    static void AddCell( Dictionary<int, List<Cell>> dict, Cell cell )
    {
        List<Cell> cells = null;
        if ( dict.TryGetValue( cell.Node1, out cells ) == false )
        {
            cells = new List<Cell>();
            dict[cell.Node1] = cells;
        }
        cells.Add( cell );
        if ( dict.TryGetValue( cell.Node2, out cells ) == false )
        {
            cells = new List<Cell>();
            dict[cell.Node2] = cells;
        }
        cells.Add( cell );
    }

    // Gets a cell from a node value
    static Cell GetCell( Dictionary<int, List<Cell>> dict, int node )
    {
        var cell = null as Cell;
        var cells = dict[node];

        if ( cells.Count > 0 )
            cell = cells.First();

        return cell;
    }

    // Removes a cell from the dictionary for both node1 and node2 entries.
    static void RemoveCell( Dictionary<int, List<Cell>> dict, Cell cell )
    {
        dict[cell.Node1].Remove( cell );
        dict[cell.Node2].Remove( cell );
    }
}

这可以用数组来代替字典完成,并且可能会有更好的性能。此外,我们可以利用字典中 List<Cell> 只能有 2 个计数的事实。我确实不会重新编写全部内容。 - lemon

1
您可以以下方式完成:

static IEnumerable<Edge<T>> OrderEdges<T>(this IEnumerable<Edge<T>> edges)
    where T : IEquatable<T>
{
    Debug.Assert(edges.Any());
    var map = new Dictionary<T, Edge<T>>();

    foreach (var e in edges)
    {
        if (map.ContainsKey(e.Node1))
        {
            Debug.Assert(!map.ContainsKey(e.Node2));
            map.Add(e.Node2, e);
        }
        else
        {
            map.Add(e.Node1, e);
        }
    }

    var current = edges.First();
    var orderedEdges = new HashSet<Edge<T>>();

    while (true)
    {
        orderedEdges.Add(current);
        yield return current;

        if (orderedEdges.Count == map.Count)
            break;

        var next = map[current.Node2];
        current = orderedEdges.Contains(next) ? map[current.Node1] : next;
    }
}

其中 Edge<T> 类如下:

class Edge<T> where T: IEquatable<T>
{
    public T Node1 { get; }
    public T Node2 { get; }
    public string Name { get; }
    public Edge(string name, T node1, T node2)
    {
        Name = name;
        Node1 = node1;
        Node2 = node2;
    }

    public override string ToString() => Name;
}

如果我们运行这个小家伙:
var edges = new List<Edge<int>>() {
    new Edge<int>("A", 4, 9), new Edge<int>("B", 6, 1),
    new Edge<int>("C", 1, 3), new Edge<int>("D", 3, 10),
    new Edge<int>("E", 7, 2), new Edge<int>("F", 4, 2),
    new Edge<int>("G", 9, 8), new Edge<int>("H", 10, 5),
    new Edge<int>("I", 7, 5), new Edge<int>("J", 6, 8) };

Console.WriteLine(string.Join(" -> ", edges.OrderEdges()));

我们得到了预期的结果:
A -> G -> J -> B -> C -> D -> H -> I -> E -> F

请注意,此解决方案假定输入数据格式良好。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接