C# 数据结构,可以动态调整大小到给定限制的列表,并允许快速访问任何索引

Posted

技术标签:

【中文标题】C# 数据结构,可以动态调整大小到给定限制的列表,并允许快速访问任何索引【英文标题】:C# data structure, list which can dynamically resize up to a given limit, and allows fast access to any index 【发布时间】:2016-11-24 18:56:46 【问题描述】:

我正在为 AI 代理实现一个内存系统。它需要有一个内部状态转换列表,上限为某个数字,例如 10000。

如果容量已满,添加新内存应该会自动删除最旧的内存。

重要的是,我还需要能够快速访问此列表中的任何项目。

Queue 的包装起初看起来很明显,但 Queue 不允许快速访问任何元素。 (O(n))

同样,从 List 结构的开头删除一个项目需要 O(n)。

LinkedLists 允许快速添加和删除,但同样不允许快速访问每个索引。

数组将允许随机访问,但显然它不能动态调整大小并且删除是有问题的。

我已经看到有人建议使用 HashMap,但我确定如何实施。

建议?

【问题讨论】:

听起来你想要一个circular buffer 是的,循环缓冲区看起来是个不错的选择。 @Lee - 将此作为实际答案 - 获得一些分数! :) 您想通过键还是通过索引来访问项目?如果按键,您可能对我的MruDictionary 感兴趣 【参考方案1】:

如果您希望队列为固定长度,您可以使用circular buffer,它启用 O(1) 的入队、出队和索引操作,并在队列已满时自动覆盖旧条目。

【讨论】:

【参考方案2】:

尝试使用DictionaryLinkedListDictionary 的键是LinkedList 节点的索引,Dictionary 的值是LinkedListNode 类型;即LinkedList 节点。

字典几乎会给你一个O(1),它的操作和删除/添加LinkedListNode(s)LinkedList的开头或结尾也是O(1)

另一种选择是使用HashTable。但是,在这种情况下,您必须事先知道表的容量(参见Hashtable.Add Method)才能获得O(1) 的性能:

如果 Count 小于 Hashtable 的容量,则此方法是 O(1) 操作。如果需要增加容量以容纳新元素,则此方法变为 O(n) 操作,其中 n 为 Count。

在第一个解决方案中,无论LinkedListDictionary 的容量是多少,您仍然可以从DictionaryLinkedList 获得几乎一个O(1)。当然,这将是O(3)O(4),具体取决于您在DictionaryLinkedList 上执行的在内存类中执行添加或删除操作的操作总数。搜索访问将始终为 O(1),因为您将仅使用 Dictionary

【讨论】:

我明白你的意思,这可能是一个愚蠢的问题,但是拥有 LinkedList 而不是仅使用整数作为键并保留最小和最大键有什么好处?每当您必须删除最旧的值时,只需删除 HashMap[min] 处的值并增加 min。每当您添加一个值时,递增 max 并将其添加到 HashMap[max]。有效键的范围始终为 [min, max]。【参考方案3】:

HashMap 用于 Java,因此最接近的等价物是 Dictionary。 C# Java HashMap equivalent。但我不会说这是最终的答案。

如果您将其实现为字典,其中键 == 内容,那么您可以使用 O(1) 搜索内容。但是,您不能拥有相同的密钥。另外,因为没有排序,所以你可能不知道第一个内容是什么。 如果您将其实现为 Dictionary,其中 key == 索引,value == 内容,搜索内容仍然需要 O(n),因为您不知道内容的位置。 如果按索引reference 搜索内容,列表或数组将花费 O(1)。所以,请仔细检查你的陈述,它需要 O(n) 如果按索引搜索就足够了,那么@Lee 提到的循环数组/缓冲区就足够了。 否则,与 DB 类似,您可能希望在 2 个单独的数据中维护:1 个用于存储数据(循环数组),另一个用于搜索(哈希)。

【讨论】:

【参考方案4】:

编辑:@Lee 说得对。循环缓冲区似乎可以满足您的需求。答案留在原地。

我认为您想要的数据结构可能是一个优先级队列——这取决于您所说的“快速访问任何项目”是什么意思。如果您的意思是“能够枚举 O(N) 中的所有项目”,那么优先级队列就符合要求。如果您的意思是“按历史顺序枚举列表”,则不会。

假设你需要这些操作;

添加项目并与时间关联 删除最旧的项目 以任意顺序枚举所有现有项目

然后你可以很容易地扩展我刚才写的这个优先队列实现。

您需要将 IEnumerable 实现为通过 T[] data 数组从 0 到 cursor 的循环。这将为您提供枚举。 实现一个 GetItem(i) 函数,只要 i <= cursor 就返回 this.data[i]

通过将其放入 Push() 方法来实现自动大小限制;

if (queue.Size => 10000) 队列.Pop();

我认为这是 O(ln n) 用于推送和弹出,而 O(N) 用于枚举所有项目,或者 O(i) 用于查找任何项目,只要您不需要按顺序排列它们。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Mindfire.DataStructures

    public class PiorityQueue<T>
    
        private int[] priorities;
        private T[] data;
        private int cursor;
        private int capacity;

        public int Size
        
            get
            
                return cursor+1;
            
        

        public PiorityQueue(int capacity)
        
            this.cursor = -1;
            this.capacity = capacity;
            this.priorities = new int[this.capacity];
            this.data = new T[this.capacity];
        

        public T Pop()
        
            if (this.Size == 0)
            
                throw new InvalidOperationException($"The this.GetType().Name is Empty");
            

            var result = this.data[0];

            this.data[0] = this.data[cursor];
            this.priorities[0] = this.priorities[cursor];
            this.cursor--;

            var loc = 0;
            while (true)
            
                var l = loc * 2;
                var r = loc * 2 + 1;
                var leftIsBigger = l <= cursor && this.priorities[loc] < this.priorities[l];
                var rightIsBigger = r <= cursor && this.priorities[loc] < this.priorities[r];

                if (leftIsBigger)
                
                    Swap(loc, l);
                    loc = l;
                
                else if (rightIsBigger)
                
                    Swap(loc, r);
                    loc = r;
                
                else
                
                    break;
                
            
            return result;
        

        public void Push(int priority, T v)
        
            this.cursor++;
            if (this.cursor == this.capacity)
            
                Resize(this.capacity * 2);
            ;
            this.data[this.cursor] = v;
            this.priorities[this.cursor] = priority;
            var loc = (this.cursor -1)/ 2;

            while (this.priorities[loc] < this.priorities[cursor])
            
                // swap
                this.Swap(loc, cursor);
            
        

        private void Swap(int a, int b)
        
            if (a == b)  return; 

            var data = this.data[b];
            var priority = this.priorities[b];
            this.data[b] = this.data[a];
            this.priorities[b] = this.priorities[a];
            this.priorities[a] = priority;
            this.data[a] = data;
        

        private void Resize(int newCapacity)
        
            var newPriorities = new int[newCapacity];
            var newData = new T[newCapacity];
            this.priorities.CopyTo(newPriorities, 0);
            this.data.CopyTo(newData, 0);
            this.data = newData;
            this.priorities = newPriorities;
            this.capacity = newCapacity;
        

        public PiorityQueue() : this(1)
        
        

        public T Peek()
        
            if (this.cursor > 0)
            
                return this.data[0];
            
            else
            
                return default(T);
            

        

        public void Push(T item, int priority)
        
        
    

【讨论】:

以上是关于C# 数据结构,可以动态调整大小到给定限制的列表,并允许快速访问任何索引的主要内容,如果未能解决你的问题,请参考以下文章

根据列表中图像的数量按比例调整动态图像列表的大小

为啥我的列表框没有调整大小? (动态调整对话框组件的大小)

调整列表大小并在列表 C# 中引用变量位置

c# winform groupbox上动态生成的控件,如何让控件随着分辨率变化而自动调整位置和大小

根据列表大小调整引导菜单多列的大小

动态调整UITableView的大小,该大小将显示为不可滚动的内容列表