解析从源码分析常见的基于Array的数据结构动态扩容机制的详解

本文的写作冲动来源于今晚看到的老赵的一则微博“大家知道System.Collections.Generic.List<T>是一种什么样的数据结构?内部的元素是怎么存放的?还有Dictionary<TKey,TValue>呢?…”。

查了一下书,如果参考数据结构和算法里介绍的线性表合哈希表的特点,非常官方的答案就类似:List<T>是一种线性的内存连续分配的存储结构,元素是顺序存放的;它的优点是内存连续分配,相对节省空间,在设定长度范围内增加元素开销很小;缺点是查找复杂度为O(n),不如哈希结构O(1)复杂度来的快,如插入节点超过指定长度需要重新开辟内存,开销很大云云。而Dictionary<TKey,TValue>则是哈希结构,优点blahblahblah缺点blahblahblah。回答结束。

然后再看老赵微博下面的回答,似乎很不统一,多数认为是基于数组实现的,但是…擦,看一圈都没有老赵满意的答案。以前看过文章听说过StringBuilder和HashTable内部是怎么实现的,以及一个笼统的列表内存扩容两倍说,但是一直不知道具体细节也不太肯定,所以我也很想知道答案。老赵说稍微有点儿好奇心的程序员都应该会去看看两个实现的源代码。世上无难事只怕有心人,要是真的有心人顺便还应该不论对错记录一下自己的学习心得,哈哈。

注:如果你是新手,建议直接到此为止,不要再往下看了。实在好奇想知道答案,最简单正确也是我的偶像老赵所推荐的做法当然是自己查MSDN和framework源码。为了不误导人,本文再加上一个标签:无责任乱写。

一、StringBuilder

StringBuilder有6种构造函数,直接通过无参构造函数创建对象比较常见。MSDN(中文)里说“此实现的默认容量是 16,默认的最大容量是 Int32.MaxValue”。默认容量是16,16什么呢,这话怎么说得这么模糊呢?反汇编跟一下源码,看到它的构造函数最终要调用一个方法:


代码如下:

public unsafe StringBuilder(string value, int startIndex, int length, int capacity)
        {
            if (capacity < 0)
            {
                throw new ArgumentOutOfRangeException("capacity", Environment.GetResourceString("ArgumentOutOfRange_MustBePositive", new object[]
    {
     "capacity"
    }));
            }
            if (length < 0)
            {
                throw new ArgumentOutOfRangeException("length", Environment.GetResourceString("ArgumentOutOfRange_MustBeNonNegNum", new object[]
    {
     "length"
    }));
            }
            if (startIndex < 0)
            {
                throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_StartIndex"));
            }
            if (value == null)
            {
                value = string.Empty;
            }
            if (startIndex > value.Length - length)
            {
                throw new ArgumentOutOfRangeException("length", Environment.GetResourceString("ArgumentOutOfRange_IndexLength"));
            }
            this.m_MaxCapacity = 2147483647;
            if (capacity == 0)
            {
                capacity = 16; //默认值
            }
            if (capacity < length)
            {
                capacity = length;
            }
            this.m_ChunkChars = new char[capacity]; //字符数组容量初始化
            this.m_ChunkLength = length;
            fixed (char* ptr = value)
            {
                StringBuilder.ThreadSafeCopy(ptr + (IntPtr)startIndex, this.m_ChunkChars, 0, length);
            }
        }

默认容量16,其实估计是指默认预分配的字符串容量为16个字符。

再分析其中带两个参数的:public StringBuilder(int capacity, int maxCapacity),它的主要实现如下:


代码如下:

public StringBuilder(int capacity, int maxCapacity)
        {
            if (capacity > maxCapacity)
            {
                throw new ArgumentOutOfRangeException("capacity", Environment.GetResourceString("ArgumentOutOfRange_Capacity"));
            }
            if (maxCapacity < 1)
            {
                throw new ArgumentOutOfRangeException("maxCapacity", Environment.GetResourceString("ArgumentOutOfRange_SmallMaxCapacity"));
            }
            if (capacity < 0)
            {
                throw new ArgumentOutOfRangeException("capacity", Environment.GetResourceString("ArgumentOutOfRange_MustBePositive", new object[]
    {
     "capacity"
    }));
            }
            if (capacity == 0)
            {
                capacity = Math.Min(16, maxCapacity); //将最大容量和默认容量16进行比较,取较小的
            }
            this.m_MaxCapacity = maxCapacity;
            this.m_ChunkChars = new char[capacity];
        }

这里就有一个疑问:通常我们实现字符串拼接的时候,肯定不能保证字符串的容量不大于默认容量16,如果大于16,StringBuilder是如何实现这种动态扩容效果的呢,总不能一下子就留足内存吧?我们看一下常见的Append(string value)方法是如何实现的:


代码如下:

public unsafe StringBuilder Append(string value)
        {
            if (value != null)
            {
                char[] chunkChars = this.m_ChunkChars;
                int chunkLength = this.m_ChunkLength;
                int length = value.Length;
                int num = chunkLength + length;
                if (num < chunkChars.Length) //没有超过最大容量
                {
                    if (length <= 2) //2个及2个以下字符拼接
                    {
                        if (length > 0)
                        {
                            chunkChars[chunkLength] = value[0];
                        }
                        if (length > 1)
                        {
                            chunkChars[chunkLength + 1] = value[1];
                        }
                    }
                    else
                    {
                        fixed (char* smem = value)
                        {
                            fixed (char* ptr = &chunkChars[chunkLength])
                            {
                                string.wstrcpy(ptr, smem, length); //好像C函数 看不到内部实现
                            }
                        }
                    }
                    this.m_ChunkLength = num;
                }
                else
                {
                    this.AppendHelper(value);
                }
            }
            return this;
        }

上面的代码中,对于超过拼接后默认最大容量的字符串的逻辑在AppendHelper中,AppendHelper最终是通过下面的方法实现的:


代码如下:

internal unsafe StringBuilder Append(char* value, int valueCount)
        {
            int num = valueCount + this.m_ChunkLength;
            if (num <= this.m_ChunkChars.Length)
            {
                StringBuilder.ThreadSafeCopy(value, this.m_ChunkChars, this.m_ChunkLength, valueCount);
                this.m_ChunkLength = num;
            }
            else
            {
                int num2 = this.m_ChunkChars.Length - this.m_ChunkLength;
                if (num2 > 0)
                {
                    StringBuilder.ThreadSafeCopy(value, this.m_ChunkChars, this.m_ChunkLength, num2);
                    this.m_ChunkLength = this.m_ChunkChars.Length;
                }
                int num3 = valueCount - num2;
                this.ExpandByABlock(num3); //终于看到扩容函数了
                StringBuilder.ThreadSafeCopy(value + (IntPtr)num2, this.m_ChunkChars, 0, num3);
                this.m_ChunkLength = num3;
            }
            return this;
        }

接着来分析扩容函数ExpandByABlock:


代码如下:

private void ExpandByABlock(int minBlockCharCount)
        {
            if (minBlockCharCount + this.Length > this.m_MaxCapacity)
            {
                throw new ArgumentOutOfRangeException("requiredLength", Environment.GetResourceString("ArgumentOutOfRange_SmallCapacity"));
            }
            int num = Math.Max(minBlockCharCount, Math.Min(this.Length, 8000)); //重新分配数组的空间计算
            this.m_ChunkPrevious = new StringBuilder(this);
            this.m_ChunkOffset += this.m_ChunkLength;
            this.m_ChunkLength = 0;
            if (this.m_ChunkOffset + num < num)
            {
                this.m_ChunkChars = null;
                throw new OutOfMemoryException();
            }
            this.m_ChunkChars = new char[num]; //数组重新分配空间
        }

从上面的直白分析我们可明显看出,实例化一个StringBuilder必然要初始化并维护一个内部数组char[] m_ChunkChars。而学过C语言和数据结构的应该都知道,数组对于它的创建需要预先给出一定的连续分配的内存空间,它并不支持在原有的内存空间的基础上去扩展,所以数组对于动态内存分配是极为不利的,但是基本的数据结构如字符串和线性表就是基于数组实现的。

接着简单看了一下其他几个常用拼接方法和索引器,内部实现大致一样,几乎都是对字符数组的操作逻辑。有兴趣大家不妨也看看源码。

分析到这里,我们可以大胆假设:StringBuilder内部实现的字符串操作最终是通过字符数组char[] m_ChunkChars进行处理的。想一想也对啊,如果StringBuildr的实现是通过String加等于减等于地拼过来接过去那就逊掉了。

不能忽视的是它的扩展容量的算法,最关键的就是下面这行代码:

int num = Math.Max(minBlockCharCount, Math.Min(this.Length, 8000));其实是非常简单的数学方法,StringBuilder内部的MaxChunkSize默认为8000,大家可以评估一下,如果进行多次拼接,字符串长度随机一点,内存分配情况会怎么样,8000这个数字有没有道理。据传说和GC内部算法一样,framework一些常用类的内部实现也遵循着平衡的设计,不知真假。

二、基于Array的可动态扩容的线性结构

大家应该都非常熟悉,就像StringBuilder一样,framework中很多集合都有动态扩容效果。比如我们熟悉的线性集合ArrayList,泛型List,Queue,Stack等等,这些都是直接基于Array而实现的。

那么基于Array的集合它们内部是如何实现动态扩容的,扩容的量是怎么控制的?也许我们早有耳闻,就是动态扩展的容量是上一次已有容量的两倍,到底是不是这样的呢?带着疑问我们挑一个最常见的集合泛型List<T>分析一下。

和StringBuilder分析法类似,我们也从构造函数和Add方法着手分析。

无参构造函数如下:


代码如下:

public List()
        {
            this._items = List<T>._emptyArray;
        }

_items是个泛型数组(private T[] _items;泛型数组好像不能这么说?),通过构造函数它肯定有默认初始容量了,_emptyArray肯定初始化分配了一定空间,猜对了吗?_emptyArray定义如下:

private static readonly T[] _emptyArray = new T[0];
看看有一个参数的:


代码如下:

public List(int capacity)
        {
            if (capacity < 0)
            {
                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
            }
            this._items = new T[capacity];
        }

这回直接给_items new了一个数组对象。

好,再来一个传入初始集合的:


代码如下:

public List(IEnumerable<T> collection)
        {
            if (collection == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
            }
            ICollection<T> collection2 = collection as ICollection<T>;
            if (collection2 != null)
            {
                int count = collection2.Count;
                this._items = new T[count];
                collection2.CopyTo(this._items, 0);
                this._size = count;
                return;
            }
            this._size = 0;
            this._items = new T[4];
            using (IEnumerator<T> enumerator = collection.GetEnumerator())
            {
                while (enumerator.MoveNext())
                {
                    this.Add(enumerator.Current);
                }
            }
        }

到这里估计大家都看到关键的地方了:每个构造函数都要对_items 变量进行初始化,而这个_items 正是一个数组(如我们所知,泛型List确实是基于数组实现的)。

再来分析下增删改查中的增加也就是Add方法:


代码如下:

public void Add(T item)
        {
            if (this._size == this._items.Length)
            {
                this.EnsureCapacity(this._size + 1); //扩容函数
            }
            this._items[this._size++] = item; //通过索引器赋值,添加元素
            this._version++;
        }

我们目标明确,终于找到了扩容函数EnsureCapacity:


代码如下:

private void EnsureCapacity(int min)
        {
            if (this._items.Length < min)
            {
                int num = (this._items.Length == 0) ? 4 : (this._items.Length * 2); //容量=当前的长度的两倍
                if (num < min)
                {
                    num = min;
                }
                this.Capacity = num; //当前数组容量赋值
            }
        }

到这里,我们看到,添加一个元素的时候,如果容量没超过预分配的数组空间大小,直接通过下面的索引器赋值:


代码如下:

public T this[int index]
        {
            [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
            get
            {
                if (index >= this._size)
                {
                    ThrowHelper.ThrowArgumentOutOfRangeException();
                }
                return this._items[index];
            }
            [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
            set
            {
                if (index >= this._size)
                {
                    ThrowHelper.ThrowArgumentOutOfRangeException();
                }
                this._items[index] = value;
                this._version++;
            }
        }

如果新增的项超过了现有数组的最大容量,通过扩容函数进行容量再分配(再new一个数组对象),在函数EnsureCapacity中,我们看到:

int num = (this._items.Length == 0) ? 4 : (this._items.Length * 2); //容量=当前的长度的两倍这里进行了数组容量的重新计算,方法很简单。最重要的是给属性Capacity赋值:

this.Capacity = num; //当前数组容量赋值这个属性的set里面包含了数组new的操作:


代码如下:

public int Capacity
        {
            [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
            get
            {
                return this._items.Length;
            }
            set
            {
                if (value < this._size)
                {
                    ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
                }
                if (value != this._items.Length)
                {
                    if (value > 0)
                    {
                        T[] array = new T[value]; //重新初始化一个数组
                        if (this._size > 0)
                        {
                            Array.Copy(this._items, 0, array, 0, this._size);
                        }
                        this._items = array; //新数组指向原数组,原数组丢弃
                        return;
                    }
                    this._items = List<T>._emptyArray;
                }
            }
        }

分析到这里我们完全可以通过伪代码总结出集合的扩容算法:


代码如下:

public void Add(T item)
{
   判断当前元素个数是否等于它预设定的容量

if(等于)
   {
       重新分配内存空间(前一次的空间乘以二)
       利用Array.Copy方法把元素复制到新分配的空间
   }

设置元素的当前位置
   currentIndex++;
   this[currentIndex] = item;
}

也许有人会问,重新分配内存空间时直接简单的乘以2会不会太草率了,这样不是容易造成内存空间的浪费吗?MS的实现也不都是非常严谨的嘛?

确实,缓冲区扩容的时候,如数据量较大,很明显会造成内存浪费,泛型List提供了一个方法叫TrimExcess用来解决内存浪费的问题:


代码如下:

public void TrimExcess()
        {
            int num = (int)((double)this._items.Length * 0.9); //当前数组空间容量乘以0.9
            if (this._size < num)
            {
                this.Capacity = this._size; //重新设置数组空间
            }
        }

原理也就是一个简单的数学方法,你在外部调用一次或多次,它就会自动帮你平衡空间节省内存了。

有一个非常常用的功能就是判断元素是否在列表里,方法名就是熟悉的Contains,看代码果然是O(n)复杂度:


代码如下:

public bool Contains(T item)
        {
            if (item == null)
            {
                for (int i = 0; i < this._size; i++)
                {
                    if (this._items[i] == null)
                    {
                        return true;
                    }
                }
                return false;
            }
            EqualityComparer<T> @default = EqualityComparer<T>.Default;
            for (int j = 0; j < this._size; j++)
            {
                if (@default.Equals(this._items[j], item))
                {
                    return true;
                }
            }
            return false;
        }

大功告成,哈哈,我惊诧极了目瞪口呆,泛型List果然也是基于数组的,而且我们现在可以理直气壮地说我很清楚它的内部扩容实现算法。此时我真想拍着肩膀对自己说,哥们你辛苦了,太有成就感了,抓紧时间多想想你喜欢的姑娘吧……我果然又红光满面了。

然后,再查看源码,发现很多核心方法的实现都直接对_items数组进行操作,并多次调用了Array的静态方法,所以我们一定不可小视数据结构数组(Array)。

反汇编查看Array的源码,发现增删改查方法相当之丰富,哪位有心人可以挖掘挖掘里面使用了多少种经典算法(比如BinarySearch听说用了二分查找),反正感觉它是被我大大低估了。至少现在我们知道,经常使用的常用数据结构如StringBuilder、ArrayList,泛型List,Queue,Stack等等都是基于Array实现的。不要小看了它,随着framework的发展,也许未来还会不断出现基于Array的新的数据结构的出现。

三、基于Array的可动态扩容的哈希结构

平时开发中经常使用的其他的一些数据结构如HashTable,泛型Dictionary,HashSet以及线程安全容器ConcurrentDictionary等等也可以动态扩容。下面从HashTable源码入手简单分析下。

先从无参构造函数开始:


代码如下:

public Hashtable()
            : this(0, 1f)
        {
        }

无参构造函数最终需要调用的构造方法如下:


代码如下:

public Hashtable(int capacity, float loadFactor)
        {
            if (capacity < 0)
            {
                throw new ArgumentOutOfRangeException("capacity", Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
            }
            if (loadFactor < 0.1f || loadFactor > 1f)
            {
                throw new ArgumentOutOfRangeException("loadFactor", Environment.GetResourceString("ArgumentOutOfRange_HashtableLoadFactor", new object[]
    {
     0.1,
     1.0
    }));
            }
            this.loadFactor = 0.72f * loadFactor; //负载因子 熟悉又陌生的0.72
            double num = (double)((float)capacity / this.loadFactor);
            if (num > 2147483647.0)
            {
                throw new ArgumentException(Environment.GetResourceString("Arg_HTCapacityOverflow"));
            }
            int num2 = (num > 3.0) ? HashHelpers.GetPrime((int)num) : 3; //初始化数组空间
            this.buckets = new Hashtable.bucket[num2]; //原来是bucket结构构造的数组
            this.loadsize = (int)(this.loadFactor * (float)num2);
            this.isWriterInProgress = false;
        }

正如我们所知,哈希函数的计算结果是一个存储单位地址,每个存储单位称为桶,而buckets不正是我们要找的存储散列函数计算结果的哈希桶吗?原来它也是个数组。这里大家看到了吗?this.buckets原来就是一个struct结构数组,心里好像一下子就有底了。

好,接着找动态扩容函数,从Add方法入手吧:

public virtual void Add(object key, object value)
        {
            this.Insert(key, value, true);
        }
跟踪到Insert方法,代码一下子变得内容丰富,但是我们直接看到了一个判断语句:

if (this.count >= this.loadsize)
            {
                this.expand();
            }
上面这个if判断一目了然,expand不就是扩展的意思吗?跟进去:

private void expand()
        {
            int prime = HashHelpers.GetPrime(this.buckets.Length * 2); //直接乘以2 好像还有个辅助调用获取素数 HashHelpers.GetPrime
            this.rehash(prime); //重新hash
        }
有个rehash函数,再跟进去:


代码如下:

private void rehash(int newsize)
        {
            this.occupancy = 0;
            Hashtable.bucket[] newBuckets = new Hashtable.bucket[newsize]; //重新定义哈希buckets
            for (int i = 0; i < this.buckets.Length; i++) //竟然是遍历旧buckets,然后填充新buckets
            {
                Hashtable.bucket bucket = this.buckets[i];
                if (bucket.key != null && bucket.key != this.buckets)
                {
                    this.putEntry(newBuckets, bucket.key, bucket.val, bucket.hash_coll & 2147483647);
                }
            }
            Thread.BeginCriticalRegion();
            this.isWriterInProgress = true;
            this.buckets = newBuckets; //旧buckets的引用改变为新填充的newBuckets
            this.loadsize = (int)(this.loadFactor * (float)newsize);
            this.UpdateVersion();
            this.isWriterInProgress = false;
            Thread.EndCriticalRegion();
        }

果然又看到了数组的重新定义和再赋值:this.buckets = newBuckets;

这不就又回到老路上来了吗?再查查Array出现的频率,哈希表很多方法的实现也还是数组操作来操作去的。到这里,忍不住想起周星驰电影里的话:打完收功。

但是还没完,有一个动态扩容容量的计算问题,是和泛型列表一样的直接乘以2吗?在expand函数里我们看到了下面这一行:

int prime = HashHelpers.GetPrime(this.buckets.Length * 2);
很显然,乘以2以后还需要一个辅助函数HashHelpers.GetPrime(看命名就知道是获取素数)的运算,HashHelpers.GetPrime代码如下:


代码如下:

internal static int GetPrime(int min)
  {
   if (min < 0)
   {
    throw new ArgumentException(Environment.GetResourceString("Arg_HTCapacityOverflow"));
   }
   for (int i = 0; i < HashHelpers.primes.Length; i++)
   {
    int num = HashHelpers.primes[i];
    if (num >= min)
    {
     return num;
    }
   }
   for (int j = min | 1; j < 2147483647; j += 2)
   {
    if (HashHelpers.IsPrime(j))
    {
     return j;
    }
   }
   return min;
  }
 }

获取素数的实现好像还蛮熟悉的。继续跟踪HashHelpers,发现它的primes数组列举了最小为3,最大为7199369的素数。如果你的哈希表容量不是特别大,3到7199369的素数足够使用了(符合二八原则),否则哈希表扩容的时候需要在2147483647这个数字范围内通过HashHelpers.IsPrime来判断并动态生成素数。所以简单地说哈希表扩容后的容量是原来的两倍并不准确,这个就和哈希算法对素数的要求有直接关系,在不太精确的情况下我们可以认为约等于两倍。

出于好奇顺便查看了一下泛型字典的源码,它的内部实现包含了buckets和entries两个结构数组,还发现哈希结构的容器通常内部算法果然都比较绕。我猜它们的实现也不同程度地利用了数组的特点,说到底应该也是基于Array实现的吧(看到没,不知道就算无责任乱写)?其他的几种常见哈希结构的容器还没有认真看,有兴趣大家可以直接查看源码详细分析一下,挑典型的一两个对比着看应该非常有效果。

欢迎大家畅所欲言说说你所知道的基于Array的其他数据结构,这里我就不再班门弄斧贻笑大方了。

思考:抛一个弱弱的疑问向大家求证:实现容器的动态特性是不是必须要基于数组呢,基于数组的实现是唯一的方式吗?

(0)

相关推荐

  • 数据结构顺序表操作示例

    复制代码 代码如下: #include<stdio.h>#include<malloc.h>#define maxsize 1024typedef char datatype;typedef struct{ datatype data[maxsize]; int last;}sequenlist; /*在第I个元素前插入数据x,元素从0开始计数*/int insert(sequenlist *L,datatype x,int i){ int j; if(L->last==ma

  • Oracle 11g Release (11.1) 索引底层的数据结构

    本文内容 B-树(B-tree) 散列(Hash) k-d 树(k-d tree) 点四叉树(Point Quadtree) 本文介绍关于 Oracle 索引的结构.大概了解 Oracle 索引底层的数据结构,从而更好地理解 Oracle 索引对增.删.改.查的性能. B-树(B-tree) 非索引的结构能满足所有需要,但自平衡的 B-树索引结构更能优化在大数据集上检索的性能.每个 B-树节点拥有多个键和指针.特定 B-树支持的一个节点中键的最大数量是那颗树的顺序.每个节点都具有一个潜在的 or

  • C#数据结构与算法揭秘五 栈和队列

    这节我们讨论了两种好玩的数据结构,栈和队列. 老样子,什么是栈, 所谓的栈是栈(Stack)是操作限定在表的尾端进行的线性表.表尾由于要进行插入.删除等操作,所以,它具有特殊的含义,把表尾称为栈顶(Top) ,另一端是固定的,叫栈底(Bottom) .当栈中没有数据元素时叫空栈(Empty Stack).这个类似于送饭的饭盒子,上层放的是红烧肉,中层放的水煮鱼,下层放的鸡腿.你要把这些菜取出来,这就引出来了栈的特点先进后出(First in last out).   具体叙述,加下图. 栈通常记

  • C#数据结构与算法揭秘三 链表

    上文我们讨论了一种最简单的线性结构--顺序表,这节我们要讨论另一种线性结构--链表. 什么是链表了,不要求逻辑上相邻的数据元素在物理存储位置上也相邻存储的线性结构称之为链表.举个现实中的例子吧,假如一个公司召开了视频会议的吧,能在北京总公司人看到上海分公司的人,他们就好比是逻辑上相邻的数据元素,而物理上不相连.这样就好比是个链表. 链表分为①单链表,②单向循环链表,③双向链表,④双向循环链表. 介绍各种各样链表之前,我们要明白这样一个概念.什么是结点.在存储数据元素时,除了存储数据元素本身的信息

  • LinkedList学习示例模拟堆栈与队列数据结构

    堆栈:先进后出First in Last Out FILO 如同一个杯子队列:先进先出 First in First out FIFO  如同一个水管 复制代码 代码如下: class Duilie{    private LinkedList link;    Duilie(){        link = new LinkedList();    }    public void myAdd(Object obj){        link.addFirst(obj);    }    pu

  • java数据结构和算法学习之汉诺塔示例

    复制代码 代码如下: package com.tiantian.algorithms;/** *    _|_1              |                | *   __|__2             |                | *  ___|___3            |                |            (1).把A上的4个木块移动到C上. * ____|____4           |                | *    

  • 数据结构课程设计-用栈实现表达式求值的方法详解

    1.需求分析设计一个程序,演示用算符优先法对算术表达式求值的过程.利用算符优先关系,实现对算术四则混合运算表达式的求值.(1)输入的形式:表达式,例如2*(3+4)     包含的运算符只能有'+' .'-' .'*' .'/' .'('. ')':(2)输出的形式:运算结果,例如2*(3+4)=14:(3)程序所能达到的功能:对表达式求值并输出 2.系统设计1.栈的抽象数据类型定义:ADT Stack{数据对象:D={ai|ai∈ElemSet,i=1,2,-,n,n≥0}数据关系:R1={<

  • python实现bitmap数据结构详解

    bitmap是很常用的数据结构,比如用于Bloom Filter中:用于无重复整数的排序等等.bitmap通常基于数组来实现,数组中每个元素可以看成是一系列二进制数,所有元素组成更大的二进制集合.对于Python来说,整数类型默认是有符号类型,所以一个整数的可用位数为31位. bitmap实现思路 bitmap是用于对每一位进行操作.举例来说,一个Python数组包含4个32位有符号整型,则总共可用位为4 * 31 = 124位.如果要在第90个二进制位上操作,则要先获取到操作数组的第几个元素,

  • C#数据结构之循环链表的实例代码

    复制代码 代码如下: public class Node    {        public object Element;        public Node Link; public Node()        {            Element = null;            Link = null;        } public Node(object theElement)        {            Element = theElement;      

  • C++ 冒泡排序数据结构、算法及改进算法

    程序代码如下: 复制代码 代码如下: // BubbleSort.cpp : 定义控制台应用程序的入口点.//#include "stdafx.h"#include <cmath>#include <iostream>using namespace std;#define  MAXNUM 20template<typename T>void Swap(T& a, T& b){    int t = a;    a = b;    b

  • C数据结构之双链表详细示例分析

    复制代码 代码如下: typedef struct node{      struct node *prior;      struct node *next;       int num;}NODE;/*******双向链表的初始化********/NODE *Init_link(void){     int i;     NODE *phead,*pb,*pi;     phead = (NODE *)malloc(sizeof(NODE));     printf("please inpu

  • 从数据结构的角度分析 for each in 比 for in 快的多

    之前听说火狐的JS引擎支持for each in的语法,例如下述的代码: 复制代码 代码如下: var arr = [10,20,30,40,50];for each(var k in arr)console.log(k); 即可直接遍历出arr数组的内容. 由于只有FireFox才支持,所以几乎所有的JS代码都不用这一特征. 不过在ActionScript里天生就支持for each的语法,不论Array还是Vector,还是Dictionary,只要是可枚举的对象都可以for in和for

  • 数据结构课程设计- 解析最少换车次数的问题详解

    问题描述: 设某城市有n个车站,并有m条公交线路连接这些车站.设这些公交车都是单向的,这n个车站被顺序编号为0~n-1.编号程序,输入该城市的公交线路数,车站个数,以及各公交线路上的各站编号.实现要求:求得从站0出发乘公交车至站n一1的最少换车次数.程序设计思路:利用输入信息构建一张有向图G(用邻接短阵g表示),有向图的顶点是车站,若有某条公交线路经i站能到达j站,就在顶点i到顶点j之间设置一条权为1的有向边<i,j).这样,从站x至站y的最少上车次数便对应于图G中从点x至点y的最短路径长度.而

  • C#数据结构与算法揭秘一

    这里,我们 来说一说C#的数据结构了. ①什么是数据结构.数据结构,字面意思就是研究数据的方法,就是研究数据如何在程序中组织的一种方法.数据结构就是相互之间存在一种或多种特定关系的数据元素的集合. 程序界有一点很经典的话,程序设计=数据结构+算法.用源代码来体现,数据结构,就是编程.他有哪些具体的关系了, (1) 集合(Set):如图 1.1(a)所示,该结构中的数据元素除了存在"同属于一个集合"的关系外,不存在任何其它关系. 集合与数学的集合类似,有无序性,唯一性,确定性. (2)

  • C#数据结构与算法揭秘二 线性结构

    上文对数据结构与算法,有了一个简单的概述与介绍,这篇文章,我们介绍一中典型数据结构--线性结构. 什么是线性结构,线性结构是最简单.最基本.最常用的数据结构.线性表是线性结构的抽象(Abstract), 线性结构的特点是结构中的数据元素之间存在一对一的线性关系. 这 种一对一的关系指的是数据元素之间的位置关系,即: (1)除第一个位置的数据元素外,其它数据元素位置的前面都只有一个数据元素: (2)除最后一个位置的数据元素外,其它数据元素位置的后面都只有一个元素.也就是说,数据元素是一个接一个的排

  • C数据结构之单链表详细示例分析

    复制代码 代码如下: #include <stdio.h>#include <stdlib.h>typedef struct type{ int num; struct type *next;}TYPE;//=============================================================// 语法格式: TYPE *init_link_head(int n)// 实现功能: 从头到尾,正序创建一个具有n个节点的链表,并对其值进行初始化//

  • 从数据结构分析看:用for each...in 比 for...in 要快些

    之前听说火狐的JS引擎支持for each in的语法,例如下述的代码: 复制代码 代码如下: var arr = [10,20,30,40,50];for each(var k in arr)    console.log(k); 即可直接遍历出arr数组的内容. 由于只有FireFox才支持,所以几乎所有的JS代码都不用这一特征. 不过在ActionScript里天生就支持for each的语法,不论Array还是Vector,还是Dictionary,只要是可枚举的对象都可以for in和

  • C#数据结构与算法揭秘四 双向链表

    首先,明白什么是双向链表.所谓双向链表是如果希望找直接前驱结点和直接后继结点的时间复杂度都是 O(1),那么,需要在结点中设两个引用域,一个保存直接前驱结点的地址,叫 prev,一个直接后继结点的地址,叫 next,这样的链表就是双向链表(Doubly Linked List).双向链表的结点结构示意图如图所示. 双向链表结点的定义与单链表的结点的定义很相似, ,只是双向链表多了一个字段 prev.其实,双向链表更像是一根链条一样,你连我,我连你,不清楚,请看图. 双向链表结点类的实现如下所示

随机推荐