﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-C++ Programmer's Cookbook-随笔分类-Data Arithmetic</title><link>http://www.cppblog.com/mzty/category/654.html</link><description>&lt;br/&gt;  
&lt;br/&gt;
&lt;a href = "http://www.cppblog.com/mzty/archive/2007/03/02/19109.html"&gt;&lt;font size = 5 color ="#00FFFF"&gt;{C++ 基础}&lt;font/&gt;&lt;/a&gt;

&lt;a href = "http://www.cppblog.com/mzty/archive/2007/08/13/29922.html"&gt;&lt;font size = 5 color ="#00FFFF"&gt;{C++ 高级}&lt;font/&gt;&lt;/a&gt;

&lt;a href = "http://www.cppblog.com/mzty/archive/2007/04/16/22064.html"&gt;&lt;font size = 5 color ="#00FFFF"&gt;{C#界面，C++核心算法}&lt;font/&gt;&lt;/a&gt;

&lt;a href = "http://www.cppblog.com/mzty/archive/2007/03/04/19163.html"&gt;&lt;font size = 5 color ="#00FFFF"&gt;{设计模式}&lt;font/&gt;&lt;/a&gt;

&lt;a href = "
http://www.cppblog.com/mzty/archive/2007/03/04/19167.html"&gt;&lt;font size = 5 color ="#FF0000"&gt;{C#基础}&lt;font/&gt;&lt;/a&gt;





</description><language>zh-cn</language><lastBuildDate>Tue, 20 May 2008 16:57:19 GMT</lastBuildDate><pubDate>Tue, 20 May 2008 16:57:19 GMT</pubDate><ttl>60</ttl><item><title>Make Your Apps Fly with the New Enterprise Performance Tool (通过新的 Enterprise Performance Tool 使应用程序飞速运行)(包含各种排序算法的实现)</title><link>http://www.cppblog.com/mzty/archive/2006/04/27/6389.html</link><dc:creator>梦在天涯</dc:creator><author>梦在天涯</author><pubDate>Thu, 27 Apr 2006 09:46:00 GMT</pubDate><guid>http://www.cppblog.com/mzty/archive/2006/04/27/6389.html</guid><wfw:comment>http://www.cppblog.com/mzty/comments/6389.html</wfw:comment><comments>http://www.cppblog.com/mzty/archive/2006/04/27/6389.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/mzty/comments/commentRss/6389.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/mzty/services/trackbacks/6389.html</trackback:ping><description><![CDATA[
		<a href="http://msdn.microsoft.com/msdnmag/issues/04/12/EnterprisePerformance/default.aspx#contents">http://msdn.microsoft.com/msdnmag/issues/04/12/EnterprisePerformance/default.aspx#contents</a>
		<br />
		<br />
		<br />中文:http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/enterpriseperformance.mspx?mfr=true<br /><br /><br /><div class="date">发布日期： 1/6/2005<span class="datePipe"> | </span>更新日期： 1/6/2005</div><div class="overview"><p><a href="http://msdn.microsoft.com/msdnmag/find/default.aspx?type=Au&amp;phrase=John%20Robbins" target="_blank"><em>John Robbins</em></a></p><p>本文基于 Visual Studio 2005 的预发布版本。文中包含的所有信息均有可能变更。</p><p>本文讨论： </p><table cellspacing="0" cellpadding="0" border="0"><tbody><tr><td class="listBullet" valign="top">•</td><td class="listItem"><p>分析器的内部工作方式 </p></td></tr><tr><td class="listBullet" valign="top">•</td><td class="listItem"><p>EPT 的灵活功能 </p></td></tr><tr><td class="listBullet" valign="top">•</td><td class="listItem"><p>一个供分析的示例应用程序</p></td></tr></tbody></table><p><b>代码可从以下位置下载：</b><br /><a href="http://download.microsoft.com/download/d/3/1/d31fff33-fd97-488f-9bbd-4b7402905716/EnterprisePerformance.exe" target="_blank"><em>EnterprisePerformance.exe</em></a> (258KB)</p><p>快速代码仍然很受欢迎。即使我用来键入本文的计算机具有足够的能力和内存，能够同时控制一座原子能发电厂、一个火星漫游计划以及美国西部上空的空中交通，并且仍然具有充足的能力来处理星际探索中的 SETI 数据包，但这并不意味着开发人员不再需要担心其代码的速度和效率。在过去进行 Win32®<sup></sup> 本机开发的日子里，我们不仅需要担心速度，而且还要担心 PC 平台上那些令人讨厌的访问冲突（对于你们这些老家伙，还有“全局保护错误”和“不可恢复的应用程序错误”）。尽管托管代码已经消除了其中的一些担心，但它只意味着您所经历的那些性能问题可能比以前更加难以捉摸。主要原因是，在使用托管代码时，我们不具有在进行本机开发时所拥有的简便的运行库视图。</p><p>有许多次，当我正在使用客户端时，我不知道如何解决恶性的性能问题。当然，这些性能问题不会出现在任何测试系统中；它们只会出现在真实世界的生产中。由于公共语言运行库 (CLR) 是黑盒，因此如果我希望找到在测试系统中重复性能问题的方法，则很难预测会发生什么事情。尽管在市场中有一些第三方商业性能工具，但这些工具中的大多数都会对系统造成过多的干扰，以至于根本不能考虑在生产系统中使用。这也就是当我看到 Microsoft 将提供一个全新的分析器 — Enterprise Performance Tool (EPT) 以作为 Visual Studio® 2005 Team Developer Edition 的一部分时，感到如此兴奋的原因。它是我可以真正考虑在生产系统中使用的第一个分析系统，因为它提供了一些非常轻便的收集性能数据的手段。因为我曾经领导过一种最畅销的商业分析器的开发工作，所以我能够理解在不产生太多系统开销的情况下收集有用分析数据的困难程度。</p><p>在本文中，我将介绍 EPT 的基本原理，并向您说明如何开始使用它。因为分析器所具有的复杂性，所以在将来某一期中，我将讨论如何使用 EPT 来跟踪您可能在同事的代码中遇到的实际性能问题（我知道您的代码非常完美！）。请记住，EPT 正处在测试阶段（我使用的是 Burton Beta 1 刷新位版本 40607.83），并且在该产品发布之前，可能会对 UI 或某些特定步骤进行更改。在对 EPT 进行介绍之前，我希望花点儿时间谈论一下分析器通常是如何工作的，以便您可以更好地了解是什么使 Enterprise Performance Tool 变得如此与众不同。</p><p><br />分析器的基本原理</p><p>在您编写分析器时，可以选择两种收集数据的方式中的一种：探测和采样。这两种方式都十分有效，但是每种方式都有它的折衷方案。探测分析器收集数据的方式是在应用程序中插入探测或挂钩，以便在程序执行该挂钩时就调用分析器运行库。要放置探测，分析器需要在编译步骤中将应用程序仪表化，重写已经编译的二进制文件，或者即时将应用程序仪表化。要查看基于 .NET 的应用程序的探测分析器方法示例，请阅读 Aleksandr Mikunov 的一篇非常出色的文章 —“<a href="http://msdn.microsoft.com/msdnmag/issues/03/09/NETProfilingAPI" target="_blank"><em>Rewrite MSIL Code On the Fly with the .NET Framework Profiling API</em></a>”（该文章摘自 <i>MSDN</i>®<i>Magazine</i> 2003 年 9 月刊）。当我开始讨论 EPT 的时候，您将看到它使用术语“仪表化”来表示探测方法。</p><p>探测分析器方法的主要优势在于，当应用程序执行时，将始终调用所插入的探测。这样，分析器运行库将对运行具有完整的认识，因此在生成关键信息（例如，函数之间的父子关系）时可以确保正确，并且分析器可以报告完美的调用树，以便您可以轻松找到花费最长时间的调用路径。使用探测分析器时，没有什么事情能够阻止开发人员只在函数入口和出口处插入探测。可以在源代码行级别放置额外的探测，以便您对函数具有完整的认识。</p><p>但是，探测分析器所提供的详细视图具有一些缺点。第一个缺点是仪表化方案使用起来可能很麻烦，并且因为它是在二进制级别重写，因此存在很多可能引入潜在错误的领域。正如您可以想像到的那样，这些探测还占用了空间，从而导致一些代码膨胀和较低的性能。对于完全仪表化的应用程序，探测分析器可能会导致速度大幅度下降，以至于几乎不可能在生产系统上运行仪表化的二进制文件，从而使您在最需要该分析器的时候却无法利用它。</p><p>正如其名称所暗示的那样，采样分析器按照预先定义的时间间隔获得应用程序中正在执行的操作的快照。大多数开发人员都没有意识到 Microsoft 总是在他们的开发环境中随附了一个采样分析器。它被称为调试器！ 如果您开始调试应用程序，并且每隔 30 秒左右就中断至调试器，则您可以注意到应用程序线程正在何处执行，以便很好地了解应用程序在一次运行过程中执行了哪些操作。我已经通过手动完成采样分析器的工作，解决了很多生产性能问题。</p><p>使采样分析器如此有价值的原因在于，它们具有比探测分析器小得多的系统开销。这意味着，您可以有更高的机会在生产系统中使用它们，而不会使服务器疲于奔命以至于停机。采样分析器的问题在于，从应用程序中获得的所有样本很有可能根本不显示任何代码。例如，如果您具有大量使用数据库的应用程序，则所有样本都可以来自数据库访问程序集的内部。最后，只抓取每个线程的当前执行指令的传统采样分析器会使得确定方法之间的父子关系变得十分困难，因而确定性能最差的执行代码途径要困难得多。</p><p><br />Enterprise Performance Tool 的基本原理</p><p>在了解分析器的操作方式之后，我就可以讨论 EPT 所采取的方式了。简单地说：它既是采样分析器，又是探测分析器（Microsoft 称之为“仪表化”）。其思想是，您在开始时将通过采样分析器来查看应用程序性能，以获得常规性能特征，以便您可以开始将注意力集中于应用程序的热点问题上。在您了解具有一些问题的程序集之后，就可以求助于仪表化分析以查看特定的问题领域，以便修复它们。当然，如果您要执行单元性能测试，则没有什么能够阻止您直接转到对特定模块进行仪表化，以便在聚焦方案中查看它们的性能。</p><p>使 EPT 采样分析器有趣的部分原因在于，您可以使用大量项目来触发样本。默认的采样点是时钟周期，并且可能是您总是使用的采样点。默认情况下，每一百万个时钟周期采样一次，但是您可以将采样间隔的时钟周期数更改为您喜欢的任何值，可是该值越小，EPT 所导致的系统开销就越大。对于生产服务器，您可以将该数字设置为某个非常高的数字（如五百万），以使系统开销保持在合理的水平，同时不会完全破坏进程中的可用性。正如您预料的那样，每五百万个时钟周期采样一次将意味着您需要使应用程序运行相当长的时间，以便在您的热点区域中获得良好的样本分布。</p><p>如果您的应用程序使用了很多内存，则可以选择让 EPT 采样分析器改为在出现页错误时触发。这样，您就可以在数据被交换出 RAM 时获得性能快照，并且可以看到是谁在执行推送操作。如果初始分析器运行表明您在代码外部的区域中花费了大量时间，则可以告诉分析器改为基于系统调用来完成采样。如果您要分析具有大量线程的多线程应用程序，则该采样统计信息会对您在从用户模式转换到内核模式（这表明某些线程可能会不必要地在内核对象上阻塞）时的数据进行拍照。您可以用于采样触发器的最后一些值是 CPU 所支持的各种性能计数器，例如，分支计数或缓存丢失。这是一个只有极少数人才确实需要的高级选项，但是如果您确实需要该数据，那么知道该数据存在也不错。</p><p>那些忙碌的 Redmontonian 还解决了调用堆栈问题 — 这是对有用的采样分析器造成障碍的最大问题之一。正如我在前面提到的那样，大多数采样分析器在采样时只是对当前正在执行的指令进行拍照。Microsoft 解决了如何将极快的堆栈遍历结合到他们的采样分析器部分中，以便您能够获得样本的好处，并且知道在执行该样本时是如何到达那里的。这使得将这些快照与代码重新关联变得更加容易。</p><p>在讨论您可以分析的应用程序之前，我想提几件您很可能觉得有趣的事情。第一件事情是，如果您认为 Microsoft 是从头开发该性能工具的，那么您只猜对了一半。在 Microsoft 内部，开发团队一直在使用 EPT 的前身（名为 Call Attribute Profiler (CAP)，它使用仪表化）和 Low Overhead Profiler (LOP) — 一个采样分析器。由于 Microsoft 开发了这些工具以收集有关应用程序（例如，操作系统和整个 Office 套件）的性能信息，因此它们甚至不会给您的应用程序带来什么负担。我曾经使用过 EPT 的前身，所以我可以告诉您公共版本使用起来会容易多少。此外，它们还具有一些极为有趣的功能（稍后我将予以讨论）。</p><p>第二个有趣的事项与 EPT 所支持的技术有关。尽管某些人可能认为由于 Microsoft 非常偏重于 .NET Framework，因此无法将 EPT 用于 Win32 应用程序或本机代码，但 EPT 团队实际上已经承诺支持所有的 Win32 本机应用程序以及 .NET 代码。这意味着，无论您使用哪种技术（ASP.NET、Windows® 窗体、MFC 或 Win32 服务），您都具有完全的采样和仪表化支持。您将看到，在 Visual Studio .NET 中，跨技术使用 EPT 没有任何差异。</p><p>实际的 EPT 设置非常平常；只需从 Visual Studio .NET 安装程序的“Enterprise Tools”树控件中选择“Enterprise Performance Tool”即可。当然，因为您知道 EPT 仍然是一个测试产品，所以您的第一个反应可能是运行虚拟 PC，并在那里安全地包含所有内容。但是，为了执行采样分析，EPT 使用内核模式设备驱动程序来响应 CPU 性能计数器中断，不过令人遗憾的是，虚拟 PC 没有实现计数器。它也没有模拟高级可编程中断控制器 (APIC)，而这两者都是内核设备驱动程序完成其工作所必需的。好消息是，如果您没有额外的计算机以便安装 EPT，那么您也并非完全不幸，因为仪表分析器仍然能够工作。如果您没有多余的计算机以便安装 EPT，那么这是一个让公司为您购买另一台计算机的好借口。</p><p><br />Animated Algorithm</p><p>要学习任何工具的用法，您都需要一个合适的示例应用程序，以便能够最佳地利用该工具。在测试周期的这一时刻，EPT 没有随附任何示例，但是在我的硬盘上已经有了一个完美的分析器示例。早些时候，我正在尝试解决如何在 Windows 窗体应用程序中使用多线程的问题，因此我编写了一个名为 Animated Algorithm 的了不起的小程序，该程序可实时激活大量排序算法。<b>图 1</b> 显示我的示例应用程序已经准备好排序。</p><div style="WIDTH: 260px"><img height="320" alt="" src="http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/art/Enterprisefig01.gif" width="260" border="0" /><br /><p class="figureCaption"><b>图 1 正在工作的 Animated Algorithm</b></p><div class="figureRule"></div></div><p>Animated Algorithm 使您可以在窗体的组合框中，从 15 个不同的排序算法中进行选择。“Options”菜单使您可以选择各个元素交换或设置之间的休眠时间，以便您可以降低图形更新的速度。</p><p>我不久前使用 Microsoft® .NET Framework 版本 1.1 编写了 Animated Algorithm，因此您不会在代码中找到任何奇特的泛型或新的 BackgroundWorker 项。NSORT 程序集中的排序算法来自由 Jonathan de Halleux、Marc Clifton 和 Robert Rohde 张贴到 The Code Project 上的一篇优秀文章（请参阅 <a href="http://www.codeproject.com/csharp/cssorters.asp" target="_blank"><em>Sorting Algorithms In C#</em></a>），该程序集将算法封装到公共结构中，以便您可以轻松地替换执行元素交换和设置的类。因为它们具有非常好的体系结构，所以我需要关心的所有内容为 UI 部分。</p><p>在本文的其余部分中，我将分析 Animated Algorithm 程序。如果 EPT 团队将该程序作为示例应用程序随附在产品中，则会非常棒。（哈哈。）</p><p><br />EPT 入门</p><p>在 Visual Studio 2005 Beta 1 中，在哪里可以找到 EPT 当然是不明显的。EPT 在您启动 Performance Wizard（它位于“Tools”菜单下）时启动，并且无论是否打开项目，它都存在。请记住，Performance Wizard 所创建的性能会话不是项目的一部分；它们实际上是具有自己的 IDE 窗口（称为 Performance Explorer）的单独文件。您可以通过从“File”|“Open”对话框中选择 PSESS 文件，来打开您创建的性能会话。</p><p>如果您在单步执行 Performance Wizard 时没有打开项目，则所产生的性能会话将与您指定的二进制文件相关联。但是，在测试版中，在您指定要运行的二进制文件时，必须打开关联的项目。我只是想顺便提一下这个小小的技巧，因为当我第一次遇到该问题时，它确实让我困惑不已。</p><p>在您启动 Performance Wizard 以后，呈现在您面前的第一个屏幕要求您选择要分析的应用程序。如果您打开了一个可生成多个程序集的项目（如 Animated Algorithm），则只能从该向导中选取一个程序集。如果要进行采样，则只选取这一个程序集是很好的，因为 EPT 采样会分析加载的所有程序集（包括那些来自框架类库的程序集）。但是，如果您要对多个程序集执行仪表化分析，则 Performance Wizard 只选择这一个程序集，因此您将需要在 Performance Explorer 中所生成的性能会话中指定其他项目或程序集。稍后我将向您说明如何完成该工作。</p><p>在选择了要在性能会话中使用的程序集或项目之后，您必须选取分析方法。在 Performance Explorer 中的任何位置，您都可以在采样和仪表化之间切换，以满足自己的需要；您在该向导页中进行的选择只表示您最初希望执行的操作。在选择了分析方法之后，向导就基本完成了。对于 EPT 的最终版本，您将在 Performance Wizard 中具有用于指定附加信息的更多选项。最终版本还将使您可以直接从 Performance Explorer 中创建性能会话。</p><p><b>图 2</b> 显示了 Performance Explorer 在刚刚完成 Performance Wizard 步骤以创建 AnimatedAlgorithms 项目的仪表化运行之后的窗口。要添加另一个项目的输出二进制文件，请右键单击“Targets”文件夹，然后从上下文菜单中选择“Add Target Project”。如果要添加与该项目没有关联的特定二进制文件，请选择另一个选项 —“Add Target Binary”。如果您已经选择了“Add Target Project”，则可以在产生的对话框中从已打开的解决方案中选择其他项目。</p><div style="WIDTH: 200px"><img height="124" alt="" src="http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/art/Enterprisefig02.gif" width="200" border="0" /><br /><p class="figureCaption"><b>图 2 Performance Explorer</b></p><div class="figureRule"></div></div><p>如果您已经选择了仪表化运行（它由绿色启动箭头下面的下拉列表框中的文本表示），则二进制文件仪表化将在程序执行之前发生。如果您不希望针对运行仪表化某个特定的二进制文件，则请右键单击该二进制文件，并取消选中“Instrument Binary”菜单选项。</p><p>如果您已经选择了采样分析，并且希望附加到某个正在运行的项目，则单击“Attach/Detach”按钮（“Start”按钮右侧的斜向箭头）将呈现“Attach Profiler to Process”对话框。通过 EPT，您可以根据需要附加到任意多的进程，以便获得对应用程序的认识。“Attach Profiler to Process”对话框还允许您从特定的二进制文件中分离分析。在将来的某一期 <i>MSDN Magazine</i> 中，我将更详细地讨论如何附加到现有的进程（特别是为了进行 ASP.NET 性能调整）。</p><p>Performance Explorer 窗口顶部的最后一个按钮是无所不在的“Properties”按钮。在启动分析运行之前，您可能希望浏览一下性能会话属性，以设置几个关键属性。第一个属性位于“General”选项卡上，它是您希望为性能会话存储性能报告的位置。在分析项目时，默认设置是将报告存储在与解决方案相同的目录中。但更好的做法是将性能会话和它们的相应报告放置在它们自己的目录中，以便您可以更容易地存储特定的运行集。这样还可以更容易地分析之前和之后的情况，以便查看您所进行的代码更改的影响。</p><p>在“General”选项卡上，您还可以在仪表化和采样分析之间切换（这会更改在 Performance Explorer 中显示的值）。在我进行的性能调整中，我喜欢将特定的会话专用于单个类型的分析，以避免出现与报告有关的混淆。没有任何事情阻止您为所有种类的特定方案（涵盖从分析类型到单个二进制文件仪表化的所有方案）创建数以百计的不同性能会话文件。我还将提一下“General”选项卡上的最后一个项，它具有一个非常诱人的名称 —“Managed Allocation Profiling”，相信这会使您感到更加好奇。在我讨论完常规分析之后，我将返回到该项。</p><p>“Performance Session”属性页上的另一个有趣的选项卡是“Sampling”选项卡（请参见<b>图 3</b>）。在这里，您可以告诉 EPT 您要执行哪种类型的采样。正如我在前面提到的那样，您对于希望如何进行采样具有非常好的控制。</p><div style="WIDTH: 360px"><img height="288" alt="" src="http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/art/Enterprisefig03.gif" width="360" border="0" /><br /><p class="figureCaption"><b>图 3 各种 EPT 采样计数器选项</b></p><div class="figureRule"></div></div><p>在执行分析运行时，EPT 会在二进制文件在硬盘中所处的位置上将其仪表化。如果您希望将仪表化的二进制文件移动到另一个位置，请选择“Performance Session”属性页中的“Binary”选项卡，然后选中“Relocate Instrumented Binaries”（它与 REBASE 样式的重定位绝对没有任何关系），并且指定您希望将更改后的二进制文件移至何处。</p><p>“Instrumentation”选项卡使您可以指定希望在仪表化发生之前和之后运行的程序。如果您需要对仪表化的二进制文件执行其他任务（例如，将其移动到全局程序集缓存中或 Web 服务器上的特定位置），则该选项卡可能很有用。“Advanced”选项卡在该测试版中未公开。最后，通过“Counters”选项卡，您可以告诉 EPT 从系统的 CPU 中收集其他数据，例如，L2 或 L3 缓存读取不中。显然，这些选项是只有少数开发人员才会需要的非常高级的选项，但是如果您确实需要它们，那么它们可以发挥巨大的作用。</p><p>在我继续讨论查看采样数据之前，我想提一下，“Performance Explorer”窗口可以根据您的需要打开任意多个性能会话。当您希望观察特定的前后方案，或者希望用不同的仪表化二进制文件执行单独的测试运行时，这一点极为有用。当您打开多个性能会话时，应当确保右键单击特定的会话，选择“Set as Current Session”以便让该会话的设置执行，然后将报告归档到它的报告节点中。</p><p><br />查看分析器数据</p><p>将性能会话设置为您希望执行的操作以后，就可以启动分析了。我将首先对 Animated Algorithm 执行采样分析，以查看我是否可以找到一些热点。从采样中获得良好数据的关键在于执行较长时间的运行。对于 Animated Algorithm，我会将 15 个排序算法中的每一个算法运行两次，并将采样设置为默认的一百万个时钟周期。</p><p>在完成某个运行之后，EPT 会将该运行的报告放到性能会话的“Reports”文件夹中。EPT 在运行期间收集原始性能数据，并将其流式传输到报告文件中（不做任何分析）。这样，您可以在运行应用程序时避免所有系统开销，但是您将为大型报告文件付出代价。我刚才完成的运行的采样报告文件大小为 3.70MB，它用了大约三分钟才完成。请确保您在运行 EPT 时具有大量的磁盘空间。</p><p>所有数据分析（它必然伴有调用堆栈的生成以及性能数字的计算）都在您打开报告文件时发生。对于测试版，在打开文件时速度可能会降低。看起来视图好像处于无限循环中，但是，如果进度栏正在报告窗口中移动，那么请您耐心一些，文件最终将弹出。</p><p>任何分析运行中的第一个视图是“Performance Report Summary”，它显示在刚刚完成的 Animated Algorithm 采样运行的<b>图 4 </b>中。不出所料，采样将发生在整个应用程序中，因此您正在查看的信息也就是您将在应用程序中看到的内容：大部分工作都发生在框架类库或操作系统内部。如果您确实在采样“Summary”视图中看到了您的一个方法，则您很可能看到了一个性能问题。</p><div style="WIDTH: 340px"><img height="292" alt="" src="http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/art/Enterprisefig04.gif" width="340" border="0" /><br /><p class="figureCaption"><b>图 4 EPT 采样性能报告摘要</b></p><div class="figureRule"></div></div><p>快速浏览一下<b>图 4</b>，您可能想知道 Inclusive Sampled 和 Exclusive Sampled 之间的区别。Exclusive Sampled 意味着该方法在取样时位于堆栈的顶部。换句话说，它是当前正在执行的函数。Inclusive Sampled 意味着该函数在取样时出现在调用堆栈中。因而，包含方法是当前正在执行的方法的调用方。</p><p>在采样方案中，一个方法在调用堆栈 (Inclusive Sampled) 中出现的次数越多，该函数在执行中花费的时间就越多，因此这里是您需要重点关注以进行性能调整的地方。对于 Exclusive Sampled 函数而言，函数在那里频繁出现表明该函数正在被频繁地调用，但是它的执行实际上可能非常快速。对于像 Animated Algorithm 这样需要进行大量图形处理的应用程序，我完全能够预料到 GDIPLUS.DLL 中的某个函数将靠近刚刚显示的列表的顶部。在<b>图 4</b> 中，位于 GDIPLUS.DLL 中偏移量 0x5B8D 处的函数（它恰好是 FLOOR 函数）被一直调用，以便计算在屏幕上的哪个位置显示某些内容。当您观察性能运行时，请确保设置符号服务器以获得可能存在的最佳信息。在撰写本文时，我使用了 EPT 的未发布版本，因而符号尚不可用。</p><p>在我跳到其他视图中以前，我希望仪表化 Animated Algorithm，并且完成与我针对采样分析器完成的运行相同的运行，以便显示仪表化运行的性能报告摘要。正如您可以猜到的那样，仪表化的运行会生成比采样运行多得多的数据。对于该运行，我仪表化了 Animated Algorithm 中的全部五个程序集，并最终得到一个 375MB 大小的会话文件。</p><p>采样和仪表化数据之间的主要区别是：采样查看整个进程空间，并且将显示框架类库或操作系统内部（换句话说，就是您在其中不具有源代码的位置）的调用。另一方面，仪表化只查看应用程序以及您在非仪表化模块上直接调用的方法。例如，如果您具有一个“Hello World!”应用程序，并且它的 Main 只调用 Console.WriteLine，则您将获得 Main 中任何工作的计时信息以及 Console.WriteLine 长度的计时信息，但是您不会获得有关 Console.WriteLine 方法的任何详细信息。</p><p><b>图 5</b> 显示了仪表化运行的性能报告摘要。第一个表“Most Called Functions”显示了频繁使用的函数。该表中的第一列被错误标记为时间；它实际上表示对该函数的调用次数。百分比列显示了对该特定函数进行的调用总次数所占的百分比。在大多数运行中，您将在这里看到框架类库或操作系统函数。如果您看到一些来自您自己的代码的函数，则您最好了解一下您为什么如此频繁地调用该特定函数。</p><div style="WIDTH: 340px"><img height="383" alt="" src="http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/art/Enterprisefig05.gif" width="340" border="0" /><br /><p class="figureCaption"><b>图 5 仪表化运行的摘要</b></p><div class="figureRule"></div></div><p>“Functions with Most Individual Work”表列出了那些花费大部分时间以仅仅执行该函数（没有任何其他函数调用）的方法。这也称为该函数的独占时间。对于测试版本，“Time”列的单位为时钟走格数。对于最终版本，单位将是毫秒。但是，我认为性能运行的实际原始单位对于分析没有用。最重要的数字是百分比。在观察性能问题时，您希望知道，与应用程序中的所有其他方法相比，哪个方法占用了最长的时间。您在观察像 3519639455 和 3492589504 这样的两个数字时，很难对它们进行什么比较。幸运的是，该表包含百分比，而我对 EPT 团队的建议是从图表中丢弃原始数据。</p><p>最后一个表“Functions Taking Longest”显示方法的实际时间（也称为跑表时间或运行时间）。分析器记录方法的入口点时间和出口点时间，并将这两个值相减。该数字涵盖了被调用的所有子方法、所有上下文切换以及该方法执行的休眠。在<b>图 5</b> 中，您可以看到 System.Windows.Forms.Application.Run 占用了最长时间，就像您对 Windows 窗体应用程序所预料的那样。尽管很多开发人员将注意力集中于独占时间，但这只是整个性能状况的一小部分。如果方法正在对数据库进行调用或者进行 Web 服务调用，则您的方法在运行时所在的线程将在等待这些调用返回数据时阻塞，从而使得该线程从 CPU 中被移走。通过密切关注方法的运行时间，您可以找到代码中正在降低应用程序运行速度的部分。</p><p>尽管摘要视图很不错，但您最感兴趣的将是查看代码在何处阻塞了系统的其余部分（对于采样运行而言），或者阻塞了应用程序的其他方法（对于仪表化运行而言）。这是“Function”视图的职责范围 — 通过单击报告窗口底部的“Function”按钮可以选择该视图。您还可以双击“Summary”视图的任何方法以跳至“Function”视图。</p><p>对于采样运行，“Function”视图显示了至少一个包含捕获中所有函数捕获的列表。对于仪表化运行，您将看到作为该运行的一部分调用的所有仪表化方法。无论您正在执行哪种类型的分析，都会在“Function”视图中显示很多数据，因此您可以对代码的状况有一点儿感觉。</p><p>默认情况下，采样“Function”视图显示“Inclusive Samples”列和“Exclusive Samples”列。由于我喜欢百分比数字，因此我右键单击了列标题以向列标题中添加“Inclusive Percent”和“Exclusive Percent”。如果您要对多进程系统进行采样，则可能希望包含其他列（例如，“Process Name”或“Process ID”），以便您可以标识哪个方法采样与哪个进程相配。您还可以在仪表化“Function”视图中设置列标题，但是您将具有不同的标题组以供选择。</p><p>在“Function”视图中分析采样运行时，我喜欢首先扫一眼“Function”视图的头几个按“Inclusive Samples”列排序的页，以了解正在执行的方法。如果我在头几个页中没有看到我的任何方法，则我会右键单击“Function”视图并选择“Group by Module”，以便获得树报告视图。当您将函数按模块分组时，按特定列排序可以正确执行 — 这是一项很不错的功能。</p><p>对于仪表化运行，“Function”视图具有更多要显示的列。如果您拥有一台 40 英寸的显示器，则无需最大化 Visual Studio .NET 窗口就应当能够看到所有这些列。对于我们中的其他人而言，查看“Function”视图的最佳方式是按 Alt + Shift + Enter 以切换到全屏幕模式。</p><p>在这些列中，“Function”视图中的仪表化运行使用我在前面解释过的“包含”和“独占”术语。但是，还有另一个使人混淆的术语：应用程序。正如我提到的那样，运行时间是从一个仪表化点到另一个仪表化点的总时间，而不管该线程可能进行了哪些上下文切换。应用程序时间的思想是 EPT 将提取出在这些上下文切换中所花费的时间，以便您可以看到您的代码在 CPU 中实际执行的时间<b>。</b><a href="http://msdn.microsoft.com/msdnmag/issues/04/12/enterpriseperformance/default.aspx?fig=true#fig6" target="_blank"><em>图 6</em></a> 列出了您将在仪表化“Function”视图中看到的不那么明显的列的定义。您可能希望将它传送到显示器上，直到 EPT 的联机帮助问世。</p><p>在观察仪表化运行的“Function”视图时，我添加了这些列以查看各种计时的百分比值，移除原始数字时间列，并且添加了两个转换列。这为我提供了有关该运行的更清晰视图。我在排序时所依据的第一个列是“% Application Exclusive Time”，因为我希望看到哪个函数正在完成大部分工作。由于仪表化在方法进行的所有子调用周围放入了探测，所以您完全有可能在该列表的顶部看到框架类库或操作系统。实际上，对于我的 Animated Algorithm 运行，System.Drawing.SolidBrush.ctor 和 System.Drawing.Brush.Dispose 在 Application Exclusive Time 百分比中被列为第一和第二，其百分比分别为 14.982% 和 14.867%。我编写的第一个函数是位于第三位的 Bugslayer.SortDisplayGraph.SorterGraph.UpdateSingleFixedElement（其百分比为 12.217%），它在图形中绘制单独的条。根据应用程序类型的不同，我在查看“Function”视图时可能会选择按其他列排序。如果存在 Web 服务或数据库调用，则我将查看 % Elapsed Inclusive Time，以便可以看到是否有特定方法卷入到长时间阻塞中。对于像 Animated Algorithm 这样的应用程序，我还将查看 Application Inclusive Time 的百分比。</p><p>基于我的仪表化运行中的上述数字，我很想查明是谁在对 SolidBrush 方法进行这些调用，因此我右键单击 .ctor 方法并选择“Show in Caller/Callee”视图，以便查看是谁在调用该方法。该视图（它对于采样分析也可用）使您一眼就可以看出目标方法的所有调用方，以及该目标方法调用的所有方法。</p><p>因为 .ctor 方法没有仪表化，所以“Caller/Callee”视图将不会显示任何被调用方，但是它显然会显示调用方。我双击了这个唯一的调用方，它恰好是具有第三高 Application Exclusive Time 并具有<a href="http://msdn.microsoft.com/msdnmag/issues/04/12/enterpriseperformance/default.aspx?fig=true#fig7" target="_blank"><em>图 7</em></a> 所示视图的 UpdateSingleFixedElement 方法。</p><p>在<a href="http://msdn.microsoft.com/msdnmag/issues/04/12/enterpriseperformance/default.aspx?fig=true#fig7" target="_blank"><em>图 7</em></a> 中，位于视图中部的下拉组合框是目标方法（在本例中为 UpdateSingleFixedElement）。方法上方的网格包含了目标方法的所有调用方（调用方）。目标方法下方的网格包含了目标方法调用以完成其工作的所有方法（被调用方）。如果您希望查看是谁调用了特定调用方，请双击该调用方方法，该方法将变为目标方法，并且您将看到原始目标方法下降到被调用方部分中。实质上，您只是将堆栈遍历了一遍。</p><p>仅仅基于<a href="http://msdn.microsoft.com/msdnmag/issues/04/12/enterpriseperformance/default.aspx?fig=true#fig7" target="_blank"><em>图 7</em></a> 中的视图，您就可以辨别出潜在的性能问题。Animated Algorithm 似乎不具有任何突出的性能问题，但是 SolidBrush .ctor 和 Dispose 占用了如此多的时间并且都在 UpdateSingleFixedElement 方法内部调用（调用了 351,872 次），这个事实表明我做了一件愚蠢的事情 — 我每次都通过该函数创建画笔，而实际上应该将其缓存。当我在将来的某一期 <i>MSDN Magazine</i> 中开始用 EPT 分析代码时，您还将看到 Animated Algorithm 的其他一些问题。</p><p>数据的最后一个常用视图是“Callstack”视图。在这里，您可以通过更具层次性的方式看到您在“Caller/Callee”窗口中观察到的调用堆栈。对于采样运行，您将在“Callstack”视图的顶层看到很多的条目，因为这些条目中的每一个都代表一个包含独占样本的唯一点。当您在采样运行中展开项时，您还将看到，在相同级别偶尔会存在一些项，这些项指示位于根部的函数具有多个引向它的调用树。根位置中显示的项是栈顶。</p><p>对于仪表化运行，“Callstack”窗口将具有与应用程序中的每个线程相对应的根元素。因为 Animated Algorithm 只有两个线程，所以您只能在树根级别看到两个项。在“Callstack”视图中，您可以看到绝对调用堆栈（从仪表化的第一个方法向下到最后一个方法），因此您可以真正了解应用程序的执行方式。我已经有很多次对我认为代码所完成的工作和代码实际上完成的工作之间的差异感到吃惊。</p><p>您可以花费大量时间在“Callstack”窗口中分析代码。当通过应用程序观察特定的踪迹时，您可以通过选择感兴趣的特定节点，向下移动，右键单击，并选择“Set Root”菜单选项，来消除大量噪音。在<a href="http://msdn.microsoft.com/msdnmag/issues/04/12/enterpriseperformance/default.aspx?fig=true#fig8" target="_blank"><em>图 8</em></a> 中，我希望查看 NSort.SwapSorter.Sort 进行的所有调用，因此将它设置为根可以消除 UI 线程的影响。</p><p>在将来的某一期中，我将更详细地讨论 EPT 显示区域中的最后两个选项卡：“Trace”和“Type”。在“Type”视图中，您可以观察已经在应用程序中分配的对象。它在测试版中有效。当我在前面讨论性能会话属性时，我提到过在“General”选项卡上有一个“Managed Allocation Profiling”部分。如果您选择“Allocations-only”单选按钮，则 EPT 会填充“Type”视图。在测试版中，报告看起来类似于其他许多工具中的报告，但是数据收集似乎不像在其他工具中那样具有如此之多的系统开销。最后，要了解 Enterprise Performance Tool 团队的想法以及有关该工具的更多信息，请确保在 <a href="http://blogs.msdn.com/profiler" target="_blank"><em>blogs.msdn.com/profiler</em></a> 查看他们的网络日记。</p><p><b>John Robbins</b> 是 Wintellect 的创始人之一，该公司是一家专门致力于 Windows 和 .NET Framework 的软件咨询、教育和开发公司。他的最新著作是“Debugging Applications for Microsoft .NET and Microsoft Windows”(Microsoft Press, 2003)。要联系 John，请访问 <a href="http://www.wintellect.com/" target="_blank"><em>www.wintellect.com</em></a>。</p></div><img src ="http://www.cppblog.com/mzty/aggbug/6389.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/mzty/" target="_blank">梦在天涯</a> 2006-04-27 17:46 <a href="http://www.cppblog.com/mzty/archive/2006/04/27/6389.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>数据结构算法集---C++语言实现</title><link>http://www.cppblog.com/mzty/archive/2005/12/24/2060.html</link><dc:creator>梦在天涯</dc:creator><author>梦在天涯</author><pubDate>Sat, 24 Dec 2005 11:22:00 GMT</pubDate><guid>http://www.cppblog.com/mzty/archive/2005/12/24/2060.html</guid><wfw:comment>http://www.cppblog.com/mzty/comments/2060.html</wfw:comment><comments>http://www.cppblog.com/mzty/archive/2005/12/24/2060.html#Feedback</comments><slash:comments>1</slash:comments><wfw:commentRss>http://www.cppblog.com/mzty/comments/commentRss/2060.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/mzty/services/trackbacks/2060.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: 数据结构算法集---C++语言实现&nbsp;作者：萧何 文章来源：C语言之家 点击数： 687 更新时间：2004-11-9这是我学数据结构编写的算法，我把他整理出来，都是基本算法，供大家学习。我使用c++面向对象形式编写，各种算法都封装在各自的类里，如果想增加功能，在相应的类里增加函数即可。我对树和图的构造也做了一些人性化设计，输入更加形象化，你可能看不懂，...&nbsp;&nbsp;<a href='http://www.cppblog.com/mzty/archive/2005/12/24/2060.html'>阅读全文</a><img src ="http://www.cppblog.com/mzty/aggbug/2060.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/mzty/" target="_blank">梦在天涯</a> 2005-12-24 19:22 <a href="http://www.cppblog.com/mzty/archive/2005/12/24/2060.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>C#排序算法大全 </title><link>http://www.cppblog.com/mzty/archive/2005/12/24/2057.html</link><dc:creator>梦在天涯</dc:creator><author>梦在天涯</author><pubDate>Sat, 24 Dec 2005 07:51:00 GMT</pubDate><guid>http://www.cppblog.com/mzty/archive/2005/12/24/2057.html</guid><wfw:comment>http://www.cppblog.com/mzty/comments/2057.html</wfw:comment><comments>http://www.cppblog.com/mzty/archive/2005/12/24/2057.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/mzty/comments/commentRss/2057.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/mzty/services/trackbacks/2057.html</trackback:ping><description><![CDATA[<TABLE cellSpacing=2 cellPadding=2 width=780 align=center border=0>
<TBODY>
<TR>
<TD height=14><FONT color=#4ba545></FONT><B>C#排序算法大全</B></TD></TR>
<TR>
<TD height=18><FONT color=#4ba545></FONT>土人</TD></TR>
<TR>
<TD height=17><FONT class=time>2004-7-21</FONT></TD></TR>
<TR>
<TD vAlign=top height=122><FONT color=#4ba545></FONT>
<P><STRONG><FONT color=#ffa500>一、冒泡排序(Bubble)</FONT></STRONG></P>
<P>using System; </P>
<P>namespace BubbleSorter<BR>{<BR>&nbsp;public class BubbleSorter<BR>&nbsp;{<BR>&nbsp;&nbsp;public void Sort(int[] list)<BR>&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;int i,j,temp;<BR>&nbsp;&nbsp;&nbsp;bool done=false;<BR>&nbsp;&nbsp;&nbsp;j=1;<BR>&nbsp;&nbsp;&nbsp;while((j&lt;list.Length)&amp;&amp;(!done))<BR>&nbsp;&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;&nbsp;done=true;<BR>&nbsp;&nbsp;&nbsp;&nbsp;for(i=0;i&lt;list.Length-j;i++)<BR>&nbsp;&nbsp;&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(list[i]&gt;list[i+1])<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;done=false;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;temp=list[i];<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;list[i]=list[i+1];<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;list[i+1]=temp;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<BR>&nbsp;&nbsp;&nbsp;&nbsp;}<BR>&nbsp;&nbsp;&nbsp;j++;<BR>&nbsp;&nbsp;&nbsp;}<BR>&nbsp;&nbsp;}<BR>&nbsp;}</P>
<P>&nbsp;public class MainClass<BR>&nbsp;{ <BR>&nbsp;&nbsp;public static void Main()<BR>&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;int[] iArrary=new int[]{1,5,13,6,10,55,99,2,87,12,34,75,33,47};<BR>&nbsp;&nbsp;&nbsp;BubbleSorter sh=new BubbleSorter();<BR>&nbsp;&nbsp;&nbsp;sh.Sort(iArrary);<BR>&nbsp;&nbsp;&nbsp;for(int m=0;m&lt;iArrary.Length;m++)<BR>&nbsp;&nbsp;&nbsp;Console.Write("{0} ",iArrary[m]); <BR>&nbsp;&nbsp;&nbsp;Console.WriteLine();<BR>&nbsp;&nbsp;}<BR>&nbsp;}<BR>}</P>
<P><STRONG><FONT color=#ffa500>二、选择排序(Selection)</FONT></STRONG></P>
<P>using System;</P>
<P>namespace SelectionSorter<BR>{<BR>&nbsp;public class SelectionSorter<BR>&nbsp;{ <BR>&nbsp;&nbsp;private int min;<BR>&nbsp;&nbsp;public void Sort(int [] list)<BR>&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;for(int i=0;i&lt;list.Length-1;i++)<BR>&nbsp;&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;min=i;<BR>&nbsp;&nbsp;&nbsp;&nbsp;for(int j=i+1;j&lt;list.Length;j++)<BR>&nbsp;&nbsp;&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;&nbsp;if(list[j]&lt;list[min])<BR>&nbsp;&nbsp;&nbsp;&nbsp;min=j;<BR>&nbsp;&nbsp;&nbsp;&nbsp;}<BR>&nbsp;&nbsp;&nbsp;int t=list[min];<BR>&nbsp;&nbsp;&nbsp;list[min]=list[i];<BR>&nbsp;&nbsp;&nbsp;list[i]=t;<BR>&nbsp;&nbsp;&nbsp;}<BR>&nbsp;&nbsp;}<BR>&nbsp;}</P>
<P>&nbsp;public class MainClass<BR>&nbsp;{ <BR>&nbsp;&nbsp;public static void Main()<BR>&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;int[] iArrary = new int[]{1,5,3,6,10,55,9,2,87,12,34,75,33,47};<BR>&nbsp;&nbsp;&nbsp;SelectionSorter ss=new SelectionSorter();<BR>&nbsp;&nbsp;&nbsp;ss.Sort(iArrary);<BR>&nbsp;&nbsp;&nbsp;for (int m=0;m&lt;iArrary.Length;m++)<BR>&nbsp;&nbsp;&nbsp;Console.Write("{0} ",iArrary[m]); <BR>&nbsp;&nbsp;&nbsp;Console.WriteLine();<BR>&nbsp;&nbsp;}<BR>&nbsp;}<BR>}</P>
<P><STRONG><FONT color=#ffa500>三、插入排序(InsertionSorter)</FONT></STRONG></P>
<P>using System;</P>
<P>namespace InsertionSorter<BR>{<BR>&nbsp;public class InsertionSorter<BR>&nbsp;{<BR>&nbsp;&nbsp;public void Sort(int [] list)<BR>&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;for(int i=1;i&lt;list.Length;i++)<BR>&nbsp;&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;int t=list[i];<BR>&nbsp;&nbsp;&nbsp;int j=i;<BR>&nbsp;&nbsp;&nbsp;&nbsp;while((j&gt;0)&amp;&amp;(list[j-1]&gt;t))<BR>&nbsp;&nbsp;&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;&nbsp;list[j]=list[j-1];<BR>&nbsp;&nbsp;&nbsp;&nbsp;--j;<BR>&nbsp;&nbsp;&nbsp;&nbsp;}<BR>&nbsp;&nbsp;&nbsp;list[j]=t;<BR>&nbsp;&nbsp;&nbsp;}<BR>&nbsp;&nbsp;}<BR>&nbsp;}</P>
<P>&nbsp;public class MainClass<BR>&nbsp;{ <BR>&nbsp;&nbsp;public static void Main()<BR>&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;int[] iArrary=new int[]{1,13,3,6,10,55,98,2,87,12,34,75,33,47};<BR>&nbsp;&nbsp;&nbsp;InsertionSorter ii=new InsertionSorter();<BR>&nbsp;&nbsp;&nbsp;ii.Sort(iArrary);<BR>&nbsp;&nbsp;&nbsp;for(int m=0;m&lt;iArrary.Length;m++)<BR>&nbsp;&nbsp;&nbsp;Console.Write("{0}",iArrary[m]); <BR>&nbsp;&nbsp;&nbsp;Console.WriteLine();<BR>&nbsp;&nbsp;}<BR>&nbsp;}<BR>}</P>
<P><STRONG><FONT color=#ffa500>四、希尔排序(ShellSorter)</FONT></STRONG></P>
<P>using System;</P>
<P>namespace ShellSorter<BR>{<BR>&nbsp;public class ShellSorter<BR>&nbsp;{<BR>&nbsp;&nbsp;public void Sort(int [] list)<BR>&nbsp;&nbsp;{<BR>&nbsp;&nbsp;int inc;<BR>&nbsp;&nbsp;for(inc=1;inc&lt;=list.Length/9;inc=3*inc+1);<BR>&nbsp;&nbsp;&nbsp;for(;inc&gt;0;inc/=3)<BR>&nbsp;&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;&nbsp;for(int i=inc+1;i&lt;=list.Length;i+=inc)<BR>&nbsp;&nbsp;&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;&nbsp;int t=list[i-1];<BR>&nbsp;&nbsp;&nbsp;&nbsp;int j=i;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;while((j&gt;inc)&amp;&amp;(list[j-inc-1]&gt;t))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;list[j-1]=list[j-inc-1];<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;j-=inc;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<BR>&nbsp;&nbsp;&nbsp;&nbsp;list[j-1]=t;<BR>&nbsp;&nbsp;&nbsp;&nbsp;}<BR>&nbsp;&nbsp;&nbsp;}<BR>&nbsp;&nbsp;}<BR>&nbsp;}</P>
<P>&nbsp;public class MainClass<BR>&nbsp;{ <BR>&nbsp;&nbsp;public static void Main()<BR>&nbsp;&nbsp;{<BR>&nbsp;&nbsp;&nbsp;int[] iArrary=new int[]{1,5,13,6,10,55,99,2,87,12,34,75,33,47};<BR>&nbsp;&nbsp;&nbsp;ShellSorter sh=new ShellSorter();<BR>&nbsp;&nbsp;&nbsp;sh.Sort(iArrary);<BR>&nbsp;&nbsp;&nbsp;for(int m=0;m&lt;iArrary.Length;m++)<BR>&nbsp;&nbsp;&nbsp;Console.Write("{0} ",iArrary[m]);<BR>&nbsp;&nbsp;&nbsp;Console.WriteLine();<BR>&nbsp;&nbsp;}<BR>&nbsp;}<BR>}&nbsp; <BR></P></TD></TR></TBODY></TABLE><img src ="http://www.cppblog.com/mzty/aggbug/2057.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/mzty/" target="_blank">梦在天涯</a> 2005-12-24 15:51 <a href="http://www.cppblog.com/mzty/archive/2005/12/24/2057.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>数据结构～～二叉树和BSTs(三)（转）</title><link>http://www.cppblog.com/mzty/archive/2005/12/24/2055.html</link><dc:creator>梦在天涯</dc:creator><author>梦在天涯</author><pubDate>Sat, 24 Dec 2005 07:39:00 GMT</pubDate><guid>http://www.cppblog.com/mzty/archive/2005/12/24/2055.html</guid><wfw:comment>http://www.cppblog.com/mzty/comments/2055.html</wfw:comment><comments>http://www.cppblog.com/mzty/archive/2005/12/24/2055.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/mzty/comments/commentRss/2055.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/mzty/services/trackbacks/2055.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: 原文链接：Part3: Binary Trees and BSTs&nbsp;本文是"考察数据结构"系列文章的第三部分，讨论的是.Net Framework基类库没有包括的常用数据结构：二叉树。就像线形排列数据的数组一样，我们可以将二叉树想象为以二维方式来存储数据。其中一种特殊的二叉树，我们称为二叉搜索树（binary search tree），简称为BST，它的数据搜索能力比一般...&nbsp;&nbsp;<a href='http://www.cppblog.com/mzty/archive/2005/12/24/2055.html'>阅读全文</a><img src ="http://www.cppblog.com/mzty/aggbug/2055.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/mzty/" target="_blank">梦在天涯</a> 2005-12-24 15:39 <a href="http://www.cppblog.com/mzty/archive/2005/12/24/2055.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>数据结构～～队列、堆栈和哈希表（二）</title><link>http://www.cppblog.com/mzty/archive/2005/12/24/2054.html</link><dc:creator>梦在天涯</dc:creator><author>梦在天涯</author><pubDate>Sat, 24 Dec 2005 07:38:00 GMT</pubDate><guid>http://www.cppblog.com/mzty/archive/2005/12/24/2054.html</guid><wfw:comment>http://www.cppblog.com/mzty/comments/2054.html</wfw:comment><comments>http://www.cppblog.com/mzty/archive/2005/12/24/2054.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/mzty/comments/commentRss/2054.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/mzty/services/trackbacks/2054.html</trackback:ping><description><![CDATA[<P>原文链接：<SPAN lang=EN-US style="FONT-SIZE: 12pt; FONT-FAMILY: 'Times New Roman'; mso-font-kerning: 1.0pt; mso-fareast-font-family: 宋体; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA"><A href="http://msdn.microsoft.com/vcsharp/default.aspx?pull=/library/en-us/dv_vstechart/html/datastructures_guide2.asp"><FONT color=#1d58d1>Part 2: The Queue, Stack, and Hashtable</FONT></A><BR></SPAN><BR></P>
<P>本文是"考察数据结构"系列文章的第二部分，考察了三种研究得最多的数据结构：队列（Queue)，堆栈（Stack)和哈希表（Hashtable)。正如我们所知，Quenu和Stack其实一种特殊的ArrayList，提供大量不同类型的数据对象的存储，只不过访问这些元素的顺序受到了限制。Hashtable则提供了一种类数组（array-like)的数据抽象，它具有更灵活的索引访问。数组需要通过序数进行索引，而Hashtable允许通过任何一种对象索引数据项。</P>
<P>目录：</P>
<P>简介</P>
<P>“排队顺序”的工作进程</P>
<P>“反排队顺序”——堆栈数据结构</P>
<P>序数索引限制</P>
<P>System.Collections.Hashtable类</P>
<P>结论</P>
<P>&nbsp;</P>
<P>简介</P>
<P>在第一部分中，我们了解了什么是数据结构，评估了它们各自的性能，并了解了选择何种数据结构对特定算法的影响。另外我们还了解并分析了数据结构的基础知识，介绍了一种最常用的数据结构：数组。</P>
<P>数组存储了同一类型的数据，并通过序数进行索引。数组实际的值是存储在一段连续的内存空间中，因此读写数组中特定的元素非常迅速。</P>
<P>因其具有的同构性及定长性，.Net Framework基类库提供了ArrayList数据结构，它可以存储不同类型的数据，并且不需要显式地指定长度。前文所述，ArrayList本质上是存储object类型的数组，每次调用Add()方法增加元素，内部的object数组都要检查边界，如果超出，数组会自动以倍数增加其长度。</P>
<P>第二部分，我们将继续考察两种类数组结构：Queue和Stack。和ArrayList相似，他们也是一段相邻的内存块以存储不同类型的元素，然而在访问数据时，会受到一定的限制。</P>
<P>之后，我们还将深入了解Hashtable数据结构。有时侯，我们可以把Hashtable看作杀一种关联数组（associative array)，它同样是存储不同类型元素的集合，但它可通过任意对象（例如string)来进行索引，而非固定的序数。</P>
<P>“排队顺序”的工作进程</P>
<P>如果你要创建不同的服务，这种服务也就是通过多种资源以响应多种请求的程序；那么当处理这些请求时，如何决定其响应的顺序就成了创建服务的一大难题。通常解决的方案有两种：</P>
<P>“排队顺序”原则</P>
<P>“基于优先等级”的处理原则</P>
<P>当你在商店购物、银行取款的时候，你需要排队等待服务。“排队顺序”原则规定排在前面的比后面的更早享受服务。而“基于优先等级”原则，则根据其优先等级的高低决定服务顺序。例如在医院的急诊室，生命垂危的病人会比病情轻的更先接受医生的诊断，而不用管是谁先到的。</P>
<P>设想你需要构建一个服务来处理计算机所接受到的请求，由于收到的请求远远超过计算机处理的速度，因此你需要将这些请求按照他们递交的顺序依此放入到缓冲区中。</P>
<P>一种方案是使用ArrayList，通过称为nextJobPos的整型变量来指定将要执行的任务在数组中的位置。当新的工作请求进入，我们就简单使用ArrayList的Add()方法将其添加到ArrayList的末端。当你准备处理缓冲区的任务时，就通过nextJobPos得到该任务在ArrayList的位置值以获取该任务，同时将nextJobPos累加1。下面的程序实现该算法：</P>
<P>using System;<BR>using System.Collections;<BR>public class JobProcessing</P>
<P>{</P>
<P>&nbsp;&nbsp; private static ArrayList jobs = new ArrayList();<BR>&nbsp;&nbsp; private static int nextJobPos = 0;<BR>&nbsp;&nbsp; public static void AddJob(string jobName)</P>
<P>&nbsp;&nbsp; {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; jobs.Add(jobName);</P>
<P>&nbsp;&nbsp; }&nbsp;&nbsp; </P>
<P>&nbsp;&nbsp; public static string GetNextJob()</P>
<P>&nbsp;&nbsp; {</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (nextJobPos &gt; jobs.Count - 1)</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return "NO JOBS IN BUFFER";</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; string jobName = (string) jobs[nextJobPos];</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; nextJobPos++;</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return jobName;</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }</P>
<P>&nbsp;&nbsp; }</P>
<P>&nbsp;&nbsp; </P>
<P>&nbsp;&nbsp; public static void Main()</P>
<P>&nbsp;&nbsp; {</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; AddJob("1");</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; AddJob("2");</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Console.WriteLine(GetNextJob());</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; AddJob("3");</P>
<P>Console.WriteLine(GetNextJob());</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Console.WriteLine(GetNextJob());</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Console.WriteLine(GetNextJob());</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Console.WriteLine(GetNextJob());</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; AddJob("4");</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; AddJob("5");</P>
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Console.WriteLine(GetNextJob());</P>
<P>&nbsp;&nbsp; }</P>
<P>}</P>
<P>&nbsp;</P>
<P>输出结果如下：</P>
<P>1</P>
<P>2</P>
<P>3</P>
<P>NO JOBS IN BUFFER</P>
<P>NO JOBS IN BUFFER</P>
<P>4</P>
<P>这种方法简单易懂，但效率却可怕得难以接受。因为，即使是任务被添加到buffer中后立即被处理，ArrayList的长度仍然会随着添加到buffer中的任务而不断增加。假设我们从缓冲区添加并移除一个任务需要一秒钟，这意味一秒钟内每调用AddJob()方法，就要调用一次ArrayList的Add()方法。随着Add()方法持续不断的被调用，ArrayList内部数组长度就会根据需求持续不断的成倍增长。五分钟后，ArrayList的内部数组增加到了512个元素的长度，这时缓冲区中却只有不到一个任务而已。照这样的趋势发展，只要程序继续运行，工作任务继续进入，ArrayList的长度自然会继续增长。</P>
<P>出现如此荒谬可笑的结果，原因是已被处理过的旧任务在缓冲区中的空间没有被回收。也即是说，当第一个任务被添加到缓冲区并被处理后，此时ArrayList的第一元素空间应该被再利用。想想上述代码的工作流程，当插入两个工作——AddJob("1")和AddJob("2")后——ArrayList的空间如图一所示：<IMG height=99 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/2-1.gif" width=450 border=0><BR>图一：执行前两行代码后的ArrayList</P>
<P>注意这里的ArrayList共有16个元素，因为ArrayList初始化时默认的长度为16。接下来，调用GetNextJob()方法，移走第一个任务，结果如图二：</P>
<P><IMG height=99 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/2-2.gif" width=450 border=0><BR>图二：调用GetNextJob()方法后的ArrayList</P>
<P>当执行AddJob(“3”)时，我们需要添加新任务到缓冲区。显然，ArrayList的第一元素空间（索引为0）被重新使用，此时在0索引处放入了第三个任务。不过别忘了，当我们执行了AddJob(“3”)后还执行了AddJob(“4”)，紧接着用调用了两次GetNextJob()方法。如果我们把第三个任务放到0索引处，则第四个任务会被放到索引2处，问题发生了。如图三：<IMG height=136 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/2-3.gif" width=450 border=0><BR>图三：将任务放到0索引时，问题发生</P>
<P>现在调用GetNextJob()，第二个任务从缓冲中移走，nextJobPos指针指向索引2。因此，当再一次调用GetNextJob()时，第四个任务会先于第三个被移走，这就有悖于与我们的“排序顺序”原则。</P>
<P>问题发生的症结在于ArrayList是以线形顺序体现任务列表的。因此我们需要将新任务添加到就任务的右恻以保证当前的处理顺序是正确的。不管何时到达ArrayList的末端，ArrayList都会成倍增长。如果产生产生未被使用的元素，则是因为调用了GetNextJob()方法。</P>
<P>解决之道是使我们的ArrayList成环形。环形数组没有固定的起点和终点。在数组中，我们用变量来维护数组的起止点。环形数组如图四所示：</P>
<P><IMG height=369 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/2-4.gif" width=450 border=0><BR>图四：环形数组图示</P>
<P>在环形数组中，AddJob()方法添加新任务到索引endPos处（译注：endPos一般称为尾指针），之后“递增”endPos值。GetNextJob()方法则根据头指针startPos获取任务，并将头指针指向null，且“递增”startPos值。我之所以把“递增”两字加上引号，是因为这里所说的“递增”不仅仅是将变量值加1那么简单。为什么我们不能简单地加1呢？请考虑这个例子：当endPos等于15时，如果endPos加1，则endPos等于16。此时调用AddJob()，它试图去访问索引为16的元素，结果出现异常IndexOutofRangeException。</P>
<P>事实上，当endPos等于15时，应将endPos重置为0。通过递增（increment）功能检查如果传递的变量值等于数组长度，则重置为0。解决方案是将变量值对数组长度值求模（取余），increment()方法的代码如下：</P>
<P>int increment(int variable)</P>
<P>{</P>
<P>&nbsp; return (variable + 1) % theArray.Length;</P>
<P>}</P>
<P>注：取模操作符，如x % y，得到的是x 除以 y后的余数。余数总是在0 到 y-1之间。</P>
<P>这种方法好处就是缓冲区永远不会超过16个元素空间。但是如果我们要添加超过16个元素空间的新任务呢？就象ArrayList的Add()方法一样，我们需要提供环形数组自增长的能力，以倍数增长数组的长度。</P>
<P>System.Collection.Queue类</P>
<P>就象我们刚才描述的那样，我们需要提供一种数据结构，能够按照“排队顺序”的原则插入和移除元素项，并能最大化的利用内存空间，答案就是使用数据结构Queue。在.Net Framework基类库中已经内建了该类——System.Collections.Queue类。就象我们代码中的AddJob()和GetNextJob()方法，Queue类提供了Enqueue()和Dequeue()方法分别实现同样的功能。</P>
<P>Queue类在内部建立了一个存放object对象的环形数组，并通过head和tail变量指想该数组的头和尾。默认状态下，Queue初始化的容量为32，我们也可以通过其构造函数自定义容量。既然Queue内建的是object数组，因此可以将任何类型的元素放入队列中。</P>
<P>Enqueue（）方法首先判断queue中是否有足够容量存放新元素。如果有，则直接添加元素，并使索引tail递增。在这里tail使用求模操作以保证tail不会超过数组长度。如果空间不够，则queue根据特定的增长因子扩充数组容量。增长因子默认值为2.0，所以内部数组的长度会增加一倍。当然你也可以在构造函数中自定义该增长因子。</P>
<P>Dequeue()方法根据head索引返回当前元素。之后将head索引指向null，再“递增”head的值。也许你只想知道当前头元素的值，而不使其输出队列（dequeue，出列），则Queue类提供了Peek()方法。</P>
<P>Queue并不象ArrayList那样可以随机访问，这一点非常重要。也就是说，在没有使前两个元素出列之前，我们不能直接访问第三个元素。（当然，Queue类提供了Contains()方法，它可以使你判断特定的值是否存在队列中。）如果你想随机的访问数据，那么你就不能使用Queue这种数据结构，而只能用ArrayList。Queue最适合这种情况，就是你只需要处理按照接收时的准确顺序存放的元素项。</P>
<P>注：你可以将Queues称为FIFO数据结构。FIFO意为先进先出（First In, First Out），其意等同于“排队顺序（First come, first served）”。</P>
<P>译注：在数据结构中，我们通常称队列为先进先出数据结构，而堆栈则为先进后出数据结构。然而本文没有使用First in ,first out的概念，而是first come ,first served。如果翻译为先进先服务，或先处理都不是很适合。联想到本文在介绍该概念时，以商场购物时需要排队为例，索性将其译为“排队顺序”。我想，有排队意识的人应该能明白其中的含义吧。那么与之对应的，对于堆栈，只有名为“反排队顺序”，来代表（First Come, Last Served）。希望各位朋友能有更好地翻译来取代我这个拙劣的词语。为什么不翻译为“先进先出”，“先进后出”呢？我主要考虑到这里的英文served，它所包含的含义很广，至少我们可以将其认为是对数据的处理，因而就不是简单地输出那么简单。所以我干脆避开这个词语的含义。<BR><BR>“反排队顺序”——堆栈数据结构</P>
<P>Queue数据结构通过使用内部存储object类型的环形数组以实现“排队顺序”的机制。Queue提供了Enqueue()和Dequeue()方法实现数据访问。“排队顺序”在处理现实问题时经常用到，尤其是提供服务的程序，例如web服务器，打印队列，以及其他处理多请求的程序。</P>
<P>在程序设计中另外一个经常使用的方式是“反排队顺序（first come,last served）”。堆栈就是这样一种数据结构。在.Net Framework基类库中包含了System.Collection.Stack类，和Queue一样，Stack也是通过存储object类型数据对象的内部环形数组来实现。Stack通过两种方法访问数据——Push(item)，将数据压入堆栈；Pop()则是将数据弹出堆栈，并返回其值。</P>
<P>一个Stack可以通过一个垂直的数据元素集合来形象地表示。当元素压入堆栈时，新元素被放到所有其他元素的顶端，弹出时则从堆栈顶端移除该项。下面两幅图演示了堆栈的压栈和出栈过程。首先按照顺序将数据1、2、3压入堆栈，然后弹出：<BR>&nbsp;<IMG height=511 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/2-5.gif" width=118 border=0><BR>图五：向堆栈压入三个元素<BR>&nbsp;<IMG height=97 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/2-6.gif" width=97 border=0><BR>图六：弹出所有元素后的Stack</P>
<P>注意Stack类的缺省容量是10个元素，而非Queue的32个元素。和Queue和ArrayList一样，Stack的容量也可以根据构造函数定制。如同ArrayList，Stack的容量也是自动成倍增长。（回忆一下：Queue可以根据构造函数的可选项设置增长因子。）</P>
<P>注：Stack通常被称为“LIFO先进后出”或“LIFO后进先出”数据结构。<BR>堆栈：计算机科学中常见的隐喻<BR>现实生活中有很多同Queue相似的例子：DMV（译注：不知道其缩写，恕我孤陋寡闻，不知其意）、打印任务处理等。然而在现实生活很难找到和Stack近似的范例，但它在各种应用程序中却是一种非常重要的数据结构。</P>
<P>设想一下我们用以编程的计算机语言，例如：C#。当执行C#程序时，CLR（公共语言运行时）将调用Stack以跟踪功能模块（译注：这里原文为function，我理解作者的含义不仅仅代表函数，事实上很多编译器都会调用堆栈以确定其地址）的执行情况。每当调用一个功能模块，相关信息就会压入堆栈。调用结束则弹出堆栈。堆栈顶端数据为当前调用功能的信息。（如要查看功能调用堆栈的执行情况，可以在Visual Studio.Net下创建一个项目，设置断点（breakpoint），在执行调试。当执行到断点时，会在调试窗口（Debug/Windows/Call Stack）下显示堆栈信息。</P>
<P>序数索引的限制</P>
<P>我们在第一部分中讲到数组的特点是同种类型数据的集合，并通过序数进行索引。即：访问第i个元素的时间为定值。（请记住此种定量时间被标记为O(1)。）</P>
<P>也许我们并没有意识到，其实我们对有序数据总是“情有独钟”。例如员工数据库。每个员工以社保号（social security number）为其唯一标识。社保号的格式为DDD-DD-DDDD（D的范围为数字0——9）。如果我们有一个随机排列存储所有员工信息的数组，要查找社保号为111-22-3333的员工，可能会遍历数组的所有元素——即执行O(n）次操作。更好的办法是根据社保号进行排序，可将其查找时间缩减为O(log n)。</P>
<P>理想状态下，我们更愿意执行O(1)次时间就能查找到某员工的信息。一种方案是建立一个巨型的数组，以实际的社保号值为其入口。这样数组的起止点为000-00-0000到999-99-9999，如下图所示：<BR>&nbsp;<IMG height=231 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/2-7.gif" width=397 border=0><BR>图七：存储所有9位数数字的巨型数组</P>
<P>如图所示，每个员工的信息都包括姓名、电话、薪水等，并以其社保号为索引。在这种方式下，访问任意一个员工信息的时间均为定值。这种方案的缺点就是空间极度的浪费——共有109，即10亿个不同的社保号。如果公司只有1000名员工，那么这个数组只利用了0.0001%的空间。（换个角度来看，如果你要让这个数组充分利用，也许你的公司不得不雇佣全世界人口的六分之一。）</P>
<P>用哈希函数压缩序数索引</P>
<P>显而易见，创建10亿个元素数组来存储1000名员工的信息是无法接受的。然而我们又迫切需要提高数据访问速度以达到一个常量时间。一种选择是使用员工社保号的最后四位来减少社保号的跨度。这样一来，数组的跨度只需要从0000到9999。图八显示了压缩后的数组。<BR>&nbsp;<IMG height=231 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/2-8.gif" width=355 border=0><BR>图八：压缩后的数组</P>
<P>此方案既保证了访问耗时为常量值，又充分利用了存储空间。选择社保号的后四位是随机的，我们也可以任意的使用中间四位，或者选择第1、3、8、9位。</P>
<P>在数学上将这种9位数转换为4位数成为哈希转换（hashing）。哈希转换可以将一个索引器空间（indexers space）转换为哈希表（hash table）。</P>
<P>哈希函数实现哈希转换。以社保号的例子来说，哈希函数H()表示为：<BR>H(x) = x 的后四位</P>
<P>哈希函数的输入可以是任意的九位社保号，而结果则是社保号的后四位数字。数学术语中，这种将九位数转换为四位数的方法称为哈希元素映射，如图九所示：<BR>&nbsp;<IMG height=254 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/2-9.gif" width=328 border=0><BR>图九：哈希函数图示</P>
<P>图九阐明了在哈希函数中会出现的一种行为——冲突（collisions）。即我们将一个相对大的集合的元素映射到相对小的集中时时，可能会出现相同的值。例如社保号中所有后四位为0000的均被映射为0000。那么000-99-0000，113-14-0000，933-66-0000，还有其他的很多都将是0000。</P>
<P>看看之前的例子，如果我们要添加一个社保号为123-00-0191的新员工，会发生什么情况？显然试图添加该员工会发生冲突，因为在0191位置上已经存在一个员工。</P>
<P>数学标注：哈希函数在数学术语上更多地被描述为f：A-&gt;B。其中|A|&gt;|B|，函数f不是一一映射关系，所以之间会有冲突。</P>
<P>显然冲突的发生会产生一些问题。在下一节，我们会看看哈希函数与冲突发生之间的关系，然后简单地犯下处理冲突的几种机制。接下来，我们会将注意力放在System.Collection.Hashtable类，并提供一个哈希表的实现。我们会了解有关Hashtable类的哈希函数，冲突解决机制，以及一些使用Hashtable的例子。</P>
<P>避免和解决冲突</P>
<P>当我们添加数据到哈希表中，冲突是导致整个操作被破坏的一个因素。如果没有冲突，则插入元素操作成功，如果发生了冲突，就需要判断发生的原因。由于冲突产生提高了代价，我们的目标就是要尽可能将冲突压至最低。</P>
<P>哈希函数中冲突发生的频率与传递到哈希函数中的数据分布有关。在我们的例子中，假定社保号是随机分配的，那么使用最后四位数字是一个不错的选择。但如果社保号是以员工的出生年份或出生地址来分配，因为员工的出生年份和地址显然都不是均匀分配的，那么选用后四位数就会因为大量的重复而导致更大的冲突。</P>
<P>注：对于哈希函数值的分析需要具备一定的统计学知识，这超出了本文讨论的范围。必要地，我们可以使用K维（k slots）的哈希表来保证避免冲突，它可以将一个随机值从哈希函数的域中映射到任意一个特定元素，并限定在1/k的范围内。（如果这让你更加的糊涂，千万别担心！）</P>
<P>我们将选择合适的哈希函数的方法成为冲突避免机制（collision avoidance），已有许多研究设计这一领域，因为哈希函数的选择直接影响了哈希表的整体性能。在下一节，我们会介绍在.Net Framework的Hashtable类中对哈希函数的使用。</P>
<P>有很多方法处理冲突问题。最直接的方法，我们称为“冲突解决机制”（collision resolution），是将要插入到哈希表中的对象放到另外一块空间中，因为实际的空间已经被占用了。其中一种最简单的方法称为“线性挖掘”（linear probing），实现步骤如下：<BR>1．&nbsp;当要插入一个新的元素时，用哈希函数在哈希表中定位；<BR>2．&nbsp;检查表中该位置是否已经存在元素，如果该位置内容为空，则插入并返回，否则转向步骤3。<BR>3．&nbsp;如果该地址为i，则检查i+1是否为空，如果已被占用，则检查i+2，依此类推，知道找到一个内容为空的位置。</P>
<P>例如：如果我们要将五个员工的信息插入到哈希表中：Alice(333-33-1234)，Bob(444-44-1234), Cal (555-55-1237), Danny (000-00-1235), and Edward (111-00-1235)。当添加完信息后，如图十所示：<BR>&nbsp;<IMG height=309 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/2-10.gif" width=355 border=0><BR>图十：有相似社保号的五位员工</P>
<P>Alice的社保号被“哈希（这里做动词用，译注）”为1234，因此存放位置为1234。接下来来，Bob的社保号也被“哈希”为1234，但由于位置1234处已经存在Alice的信息，所以Bob的信息就被放到下一个位置——1235。之后，添加Cal，哈希值为1237，1237位置为空，所以Cal就放到1237处。下一个是Danny，哈希值为1235。1235已被占用，则检查1236位置是否为空。既然为空，Danny就被放到那儿。最后，添加Edward的信息。同样他的哈希好为1235。1235已被占用，检查1236，也被占用了，再检查1237，直到检查到1238时，该位置为空，于是Edward被放到了1238位置。</P>
<P>搜索哈希表时，冲突仍然存在。例如，如上所示的哈希表，我们要访问Edward的信息。因此我们将Edward的社保号111-00-1235哈希为1235，并开始搜索。然而我们在1235位置找到的是Bob，而非Edward。所以我们再搜索1236，找到的却是Danny。我们的线性搜索继续查找知道找到Edward或找到内容为空的位置。结果我们可能会得出结果是社保号为111-00-1235的员工并不存在。</P>
<P>线性挖掘虽然简单，但并是解决冲突的好的策略，因为它会导致同类聚合（clustering）。如果我们要添加10个员工，他们的社保号后四位均为3344。那么有10个连续空间，从3344到3353均被占用。查找这10个员工中的任一员工都要搜索这一簇位置空间。而且，添加任何一个哈希值在3344到3353范围内的员工都将增加这一簇空间的长度。要快速查询，我们应该让数据均匀分布，而不是集中某几个地方形成一簇。</P>
<P>更好的挖掘技术是“二次挖掘”（quadratic probing），每次检查位置空间的步长以平方倍增加。也就是说，如果位置s被占用，<SPAN style="FONT-SIZE: 10.5pt; FONT-FAMILY: 宋体; mso-font-kerning: 1.0pt; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 12.0pt">则首先检查</SPAN><SPAN lang=EN-US style="FONT-SIZE: 10.5pt; FONT-FAMILY: 'Times New Roman'; mso-font-kerning: 1.0pt; mso-fareast-font-family: 宋体; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA; mso-bidi-font-size: 12.0pt">s+1<SUP>2</SUP></SPAN><SPAN style="FONT-SIZE: 10.5pt; FONT-FAMILY: 宋体; mso-font-kerning: 1.0pt; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 12.0pt">处，然后检查</SPAN><SPAN lang=EN-US style="FONT-SIZE: 10.5pt; FONT-FAMILY: 'Times New Roman'; mso-font-kerning: 1.0pt; mso-fareast-font-family: 宋体; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA; mso-bidi-font-size: 12.0pt">s-1<SUP>2</SUP></SPAN><SPAN style="FONT-SIZE: 10.5pt; FONT-FAMILY: 宋体; mso-font-kerning: 1.0pt; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 12.0pt">，</SPAN><SPAN lang=EN-US style="FONT-SIZE: 10.5pt; FONT-FAMILY: 'Times New Roman'; mso-font-kerning: 1.0pt; mso-fareast-font-family: 宋体; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA; mso-bidi-font-size: 12.0pt">s+2<SUP>2</SUP></SPAN><SPAN style="FONT-SIZE: 10.5pt; FONT-FAMILY: 宋体; mso-font-kerning: 1.0pt; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 12.0pt">，</SPAN><SPAN lang=EN-US style="FONT-SIZE: 10.5pt; FONT-FAMILY: 'Times New Roman'; mso-font-kerning: 1.0pt; mso-fareast-font-family: 宋体; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA; mso-bidi-font-size: 12.0pt">s-2<SUP>2</SUP></SPAN><SPAN style="FONT-SIZE: 10.5pt; FONT-FAMILY: 宋体; mso-font-kerning: 1.0pt; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA; mso-bidi-font-family: 'Times New Roman'; mso-bidi-font-size: 12.0pt">，</SPAN><SPAN lang=EN-US style="FONT-SIZE: 10.5pt; FONT-FAMILY: 'Times New Roman'; mso-font-kerning: 1.0pt; mso-fareast-font-family: 宋体; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA; mso-bidi-font-size: 12.0pt">s+3<SUP>2 </SUP></SPAN>依此类推，而不是象线性挖掘那样从s+1，s+2……线性增长。当然二次挖掘同样会导致同类聚合。</P>
<P>下一节我们将介绍第三种冲突解决机制——二度哈希，它被应用在.Net Framework的哈希表类中。</P>
<P>System.Collections.Hashtable 类<BR>.Net Framework 基类库包括了Hashtable类的实现。当我们要添加元素到哈希表中时，我们不仅要提供元素（item），还要为该元素提供关键字（key）。Key和item可以是任意类型。在员工例子中，key为员工的社保号，item则通过Add()方法被添加到哈希表中。</P>
<P>要获得哈希表中的元素（item），你可以通过key作为索引访问，就象在数组中用序数作为索引那样。下面的C#小程序演示了这一概念。它以字符串值作为key添加了一些元素到哈希表中。并通过key访问特定的元素。</P>
<P>using System;<BR>using System.Collections;</P>
<P>public class HashtableDemo<BR>{<BR>&nbsp;&nbsp; private static Hashtable ages = new Hashtable();</P>
<P>&nbsp;&nbsp; public static void Main()<BR>&nbsp;&nbsp; {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Add some values to the Hashtable, indexed by a string key<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ages.Add("Scott", 25);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ages.Add("Sam", 6);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ages.Add("Jisun", 25);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Access a particular key<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (ages.ContainsKey("Scott"))<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int scottsAge = (int) ages["Scott"];<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Console.WriteLine("Scott is " + scottsAge.ToString());<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Console.WriteLine("Scott is not in the hash table...");<BR>&nbsp;&nbsp; }<BR>}<BR>程序中的ContainsKey()方法，是根据特定的key判断是否存在符合条件的元素，返回布尔值。Hashtable类中包含keys属性（property），返回哈希表中使用的所有关键字的集合。这个属性可以通过遍历访问，如下：</P>
<P>// Step through all items in the Hashtable<BR>foreach(string key in ages.Keys)<BR>Console.WriteLine("Value at ages[\"" + key + "\"] = " + ages[key].ToString());</P>
<P>要认识到插入元素的顺序和关键字集合中key的顺序并不一定相同。关键字集合是以存储的关键字对应的元素为基础，上面的程序的运行结果是：</P>
<P>Value at ages["Jisun"] = 25<BR>Value at ages["Scott"] = 25<BR>Value at ages["Sam"] = 6</P>
<P>即使插入到哈希表中的顺序是：Scott，Sam， Jisun。</P>
<P>Hashtable类的哈希函数</P>
<P>Hashtable类中的哈希函数比我们前面介绍的社保号的哈希值更加复杂。首先，要记住的是哈希函数返回的值是序数。对于社保号的例子来说很容易办到，因为社保号本身就是数字。我们只需要截取其最后四位数，就可以得到合适的哈希值。然而Hashtable类中可以接受任何类型的值作为key。就象上面的例子，key是字符串类型，如“Scott”或“Sam”。在这样一个例子中，我们自然想明白哈希函数是怎样将string转换为数字的。</P>
<P>这种奇妙的转换应该归功于GetHashCode()方法，它定义在System.Object类中。Object类中GetHashCode()默认的实现是返回一个唯一的整数值以保证在object的生命期中不被修改。既然每种类型都是直接或间接从Object派生的，因此所以object都可以访问该方法。自然，字符串或其他类型都能以唯一的数字值来表示。</P>
<P>Hashtable类中的对于哈希函数的定义如下：</P>
<P>H(key) = [GetHash(key) + 1 + (((GetHash(key) &gt;&gt; 5) + 1) % (hashsize – 1))] % hashsize<BR></P>
<P>这里的GetHash(key)，默认为对key调用GetHashCode()方法的返回值（虽然在使用Hashtable时，你可以自定义GetHash()函数）。GetHash(key)&gt;&gt;5表示将得到key的哈希值，向右移动5位，相当于将哈希值除以32。%操作符就是之前介绍的求模运算符。Hashsize指的是哈希表的长度。因为要进行求模，因此最后的结果H（k）在0到hashsize-1之间。既然hashsize为哈希表的长度，因此结果总是在可以接受的范围内。</P>
<P>Hashtable类中的冲突解决方案</P>
<P>当我们在哈希表中添加或获取一个元素时，会发生冲突。插入元素时，必须查找内容为空的位置，而获取元素时，即使不在预期的位置处，也必须找到该元素。前面我们简单地介绍了两种解决冲突的机制——线性和二次挖掘。在Hashtable类中使用的是一种完全不同的技术，成为二度哈希（rehasing）(有的资料也将其称为双精度哈希double hashing)。</P>
<P>二度哈希的工作原理如下：有一个包含多个哈希函数（H1……Hn）的集合。当我们要从哈希表中添加或获取元素时，首先使用哈希函数H1。如果导致冲突，则尝试使用H2，一直到Hn。各个哈希函数极其相似，不同的是它们选用的乘法因子。通常，哈希函数Hk的定义如下：<BR>Hk(key) = [GetHash(key) + k * (1 + (((GetHash(key) &gt;&gt; 5) + 1) % (hashsize – 1)))] % hashsize</P>
<P>注：运用二度哈希重要的是在执行了hashsize次挖掘后，哈希表中的每一个位置都确切地被有且仅有一次访问。也就是说，对于给定的key，对哈希表中的同一位置不会同时使用Hi和Hj。在Hashtable类中使用二度哈希公式，其保证为：(1 + (((GetHash(key) &gt;&gt; 5) + 1) % (hashsize – 1))与hashsize两者互为素数。（两数互为素数表示两者没有共同的质因子。）如果hashsize是一个素数，则保证这两个数互为素数。</P>
<P>二度哈希较前两种机制较好地避免了冲突。</P>
<P>调用因子（load factors）和扩充哈希表</P>
<P>Hashtable类中包含一个私有成员变量loadFactor，它指定了哈希表中元素个数与表位置总数之间的最大比例。例如：loadFactor等于0.5，则说明哈希表中只有一半的空间存放了元素值，其余一半皆为空。</P>
<P>哈希表的构造函数以重载的方式，允许用户指定loadFactor值，定义范围为0.1到1.0。要注意的是，不管你提供的值是多少，范围都不超过72%。即使你传递的值为1.0，Hashtable类的loadFactor值还是0.72。微软认为loadFactor的最佳值为0.72，因此虽然默认的loadFactor为1.0，但系统内部却自动地将其改变为0.72。所以，建议你使用缺省值1.0（事实上是0.72，有些迷惑，不是吗？）</P>
<P>注：我花了好几天时间去咨询微软的开发人员为什么要使用自动转换？我弄不明白，为什么他们不直接规定值为0.072到0.72之间。最后我从编写Hashtable类的开发团队的到了答案，他们非常将问题的缘由公诸于众。事实上，这个团队经过测试发现如果loadFactor超过了0.72，将会严重的影响哈希表的性能。他们希望开发人员能够更好地使用哈希表，但却可能记不住0.72这个无规律数，相反如果规定1.0为最佳值，开发者会更容易记住。于是，就形成现在的结果，虽然在功能上有少许牺牲，但却使我们能更加方便地使用数据结构，而不用感到头疼。</P>
<P>向Hashtable类添加新元素时，都要进行检查以保证元素与空间大小的比例不会超过最大比例。如果超过了，哈希表空间将被扩充。步骤如下：<BR>1．&nbsp;哈希表的位置空间近似地成倍增加。准确地说，位置空间值从当前的素数值增加到下一个最大的素数值。（回想一下前面讲到的二度哈希的工作原理，哈希表的位置空间值必须是素数。）<BR>2．&nbsp;既然二度哈希时，哈希表中的所有元素值将依赖于哈希表的位置空间值，所以表中所有值也需要二度哈希（因为在第一步中位置空间值增加了）。</P>
<P>幸运的是，Hashtable类中的Add()方法隐藏了这些复杂的步骤，你不需要关心它的实现细节。</P>
<P>调用因子（load factor）对冲突的影响决定于哈希表的总体长度和进行挖掘操作的次数。Load factor越大，哈希表越密集，空间就越少，比较于相对稀疏的哈希表，进行挖掘操作的次数就越多。如果不作精确地分析，当冲突发生时挖掘操作的预期次数大约为1/(1-lf)，这里lf指的是load factor。</P>
<P>如前所述，微软将哈希表的缺省调用因子设定为0.72。因此对于每次冲突，平均挖掘次数为3.5次。既然该数字与哈希表中实际元素个数无关，因此哈希表的渐进访问时间为O（1），显然远远好于数组的O(n)。</P>
<P>最后，我们要认识到对哈希表的扩充将以性能损耗为代价。因此，你应该预先估计你的哈希表中最后可能会容纳的元素总数，在初始化哈希表时以合适的值进行构造，以避免不必要的扩充。<BR></P><img src ="http://www.cppblog.com/mzty/aggbug/2054.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/mzty/" target="_blank">梦在天涯</a> 2005-12-24 15:38 <a href="http://www.cppblog.com/mzty/archive/2005/12/24/2054.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>数据结构简介(一）</title><link>http://www.cppblog.com/mzty/archive/2005/12/24/2053.html</link><dc:creator>梦在天涯</dc:creator><author>梦在天涯</author><pubDate>Sat, 24 Dec 2005 07:36:00 GMT</pubDate><guid>http://www.cppblog.com/mzty/archive/2005/12/24/2053.html</guid><wfw:comment>http://www.cppblog.com/mzty/comments/2053.html</wfw:comment><comments>http://www.cppblog.com/mzty/archive/2005/12/24/2053.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/mzty/comments/commentRss/2053.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/mzty/services/trackbacks/2053.html</trackback:ping><description><![CDATA[<DIV class=postTitle><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><BR>&nbsp;</DIV>
<DIV class=postText>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">第一部分</SPAN><SPAN lang=EN-US>:</SPAN><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">数据结构简介<BR></P></SPAN>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><SPAN lang=EN-US><?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /><o:p></o:p></SPAN>&nbsp;</P><SPAN lang=EN-US><o:p>
<P><SPAN lang=EN-US style="FONT-SIZE: 12pt">原文链接：<A href="http://msdn.microsoft.com/vcsharp/default.aspx?pull=/library/en-us/dv_vstechart/html/datastructures_guide.asp">Part 1: An Introduction to Data Structures<o:p></o:p></A></SPAN></P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">&nbsp;</o:p></SPAN></P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><FONT face="Times New Roman">介绍:<BR>本文是介绍在.Net平台下使用数据结构的系列文章,共分为六部分,这是本文的第一部分.本文试图考察几种数据结构,其中有的包含在.Net Framework的基类库中,有的是我们自己创建的.如果你对这些名词不太熟悉,那么我们可以把数据结构看作是一种抽象结构或是类,它通常用来组织数据,并提供对数据的操作.最常见并为我们所熟知的数据结构就是数组array,它包含了一组连续的数据,并通过索引进行访问.</FONT></SPAN></P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><FONT face="Times New Roman">在阅读本文内容之前,让我们先看看这六部分的主要内容.如果你有什么想法,或觉得本文有什么遗漏之处,希望你通过e-mail(<A href="mailto:mitchell@4guysfromrolla.com">mitchell@4guysfromrolla.com</A>)和我联系,共同分享你的思想.假如有时间的话,我很高兴将你的建议放到合适的部分,如有必要,可以在这篇系列文章中加上第七部分.</FONT></SPAN></P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><FONT face="Times New Roman">第一部分:首先介绍数据结构在算法设计中的重要性.决定数据结构的优劣在于其性能.我们将经过严格分析数据结构的各种性能.此部分还将介绍.Net Frameword下两种常用的数据机构:Array 和ArrayList.我们将考察其结构的操作方式及其效率.</FONT></SPAN></P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><FONT face="Times New Roman">第二部分:我们将继续从更多细节上分析ArrayList结构,同时还将介绍Queue类和Stack类.和ArrayList一样,Queue和Stack存放的都是一组连续的数据集合,都属于.Net Framework基类库.与ArrayList不同的是,Stack和Queue只能以预先规定的序列顺序读取其数据(先进先出和先进后出),而ArrayList可以任意获取数据项.我们将通过示例程序来考察Queue,Stack,并通过扩展ArrayList类来实现它们.之后,我们还要分析哈希表HashTable,它象ArrayList一样可以直接访问数据,不同的是它以key(字符串)为索引.</FONT></SPAN></P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><FONT face="Times New Roman">ArrayList对数据直接读取和存储是一种理想的数据结构,同时,它也是支持数据搜索的候选方案.在第三部分,我们将考察二叉树结构,对于数据搜索而言,它比ArrayList更加有效. .Net Framework并不包含此种内置数据结构,因此需要我们自己创建.</FONT></SPAN></P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><FONT face="Times New Roman">二叉树搜索的效率受制于插入到树中的数据的顺序.如果我们插入的是有序或近似有序的数据,实际上,它的效率不如ArrayList.为了将这两种的优势结合起来,在第四部分,我门将考察一种有趣的随机数据结构——SkipList. SkipList既保留了二叉树搜索的高效率,同时输入数据的顺序对其效率影响甚微.</FONT></SPAN></P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><FONT face="Times New Roman">第五部分我们将注意力转向通常用来表现图形的数据结构.图(graph)是众多节点以及节点之间边的集合.举例来说,地图就可以图的形式来表现.城市是节点,公路则是连接节点之间的边.许多现实问题都可以抽象成图的形式,因此,图也是我们经常要用到的数据结构.</FONT></SPAN></P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><FONT face="Times New Roman">最后,第六部分我们将谈到reprisent sets(表示集?)和disjoint sets(非关联集,即交集为空?)集合是一种无序数据的集中.非关联集是指它和另外一个集合没有共同的元素.我们在程序编写时会经常用到集合和非关联集.我们将在这一部分中详细描述它.</FONT></SPAN></P><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><FONT face="Times New Roman">
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><BR>数据结构性能分析</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">当我们在思考一个特别的应用程序或者程序的问题时,多数开发人员(包括我自己)都将兴趣集中到算法上以解决手头的难题,或者为应用程序加上一个很酷的特色以丰富用户的经验.我们似乎很少听到有人会为他所使用的数据结构而激动不已,啧啧赞叹. 然而,用在一个特定算法中的数据结构能够很大程度上影响其性能.最常见的例子就是在数据结构中查找一个元素.在数组中,查找过程所耗时间是与这个数组中元素的个数是成正比的.采用二叉数或者SkipLists(我找不到合适的翻译,按前所述,它包含了随机数的集合,也许看了后面的部分会想到合适的中文),耗时与数据个数比例成线型下降(sub-linear,我又黔驴词穷了).当我们要搜索大量的数据时,数据结构的选择对程序的性能尤其重要,其差别甚至达到数秒,乃至于数分钟.</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">既然在算法中使用的数据结构影响了算法的效率,因此比较各种数据结构的效率并从中选择一种更佳的方法就显得尤为重要.作为开发者而言,我们首先要关注的是随着存储的数据量的增长,数据结构性能是怎样随之改变的的?也就是说,每当数据结构中添加一个新元素时,它将怎样影响数据结构的运行时间?</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">考虑这样一种情形,我们在程序中使用了System.IO.Directory.GetFiles(路径)方法以返回文件的列表,存放到一个特定的字符串数组directory中.假设你需要搜索这个数组以判断在文件列表中是否存在XML文件(即扩展名为.xml的文件),一种方法是扫描(scan,或者是遍历)整个数组,当找到XML文件时,就设置一个标识.代码可能是这样:</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">using System;<BR>using System.Collections;<BR>using System.IO;</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">public class MyClass<BR>{<BR>&nbsp;&nbsp; public static void Main()<BR>&nbsp;&nbsp; {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; string [] fs = Directory.GetFiles(@"C:\Inetpub\wwwroot");<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bool foundXML = false;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int i = 0;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (i = 0; i &lt; fs.Length; i++)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (String.Compare(Path.GetExtension(fs[i]), ".xml", true) == 0)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; foundXML = true;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<BR>&nbsp;&nbsp; <BR>&nbsp;&nbsp;&nbsp;&nbsp; if (foundXML)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Console.WriteLine("XML file found - " + fs[i]);<BR>&nbsp;&nbsp;&nbsp;&nbsp; else<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Console.WriteLine("No XML files found.");<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <BR>&nbsp;&nbsp; }<BR>}</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><BR>现在我们来看看最糟糕的一种情况,当这个列表中不存在XML文件或者XML文件是在列表的最后,我们将会搜索完这个数组的所有元素.再来分析一下数组的效率,我们必须问问自己,"假设数组中现有n个元素,如果我添加一个新元素,增长为n+1个元素,那么新的运行时间是多少?(术语"运行时间"--running time,不能顾名思义地认为是程序运行所消耗的绝对时间,而指的是程序完成该任务所必须执行的步骤数.以数组而言,运行时间特定被认为是访问数组元素所需执行的步骤数。)要搜索数组中的一个值，潜在的可能是访问数组的每一个元素，如果数组中有n+1个元素，就将执行n+1次检查。那就是说，搜索数组耗费的时间与数组元素个数成几何线形比。</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">当数据结构的长度趋于无穷大时，分析其结构的效率，我们把这种分析方法称为渐进分析（asymptotic analysis)。渐进分析中常用的符号是大写的O（big-Oh)，以O(n)的形式描述遍历数组的性能。O是术语学中big-Oh符号的表示，n则代表遍历数组时随长度增长而与之线形增长的程序执行步数。</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">计算代码块中算法的运行时间的一种系统方法应遵循以下步骤：</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">1、判断组成算法运行时间的步骤。如前所述，对于数组而言，典型的步骤应是对数组进行读写访问的操作。而对于其他数据结构则不尽然。特别地，你应该考虑的是数据结构自身的步骤，而与计算机内部的操作无关。以上面的代码块为例，运行时间应该只计算访问数组的次数，而不用考虑创建和初始化变量以及比较两个字符串是否相等的时间。<BR>2、找到符合计算运行时间条件的代码行。在这些行上面置1。<BR>3、判断这些置1的行是否包含在循环中，如果是，则将1改为1乘上循环执行的最大次数。如果嵌套两重或多重循环，继续对循环做相同的乘法。<BR>4、找到对每行写下的最大值，它就是运行时间。</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">现在我们按照这种步骤来标记上面的代码块。首先我们已经能够确定与计算运行时间有关的代码行，再根据步骤2，在数组fs被访问的两行代码作上标记，一行是数组元素作为String.Compare()方法的参数，一行是在Console.WriteLine()方法中。我们将这两行标记为1。然后根据步骤3，String.Compare()方法是在循环中，最大循环次数为n（因为数组长度为n）。因此将该行的标记1改为n。最后，我们得到的运行时间就是标记的最大值n，记为O(n)。（译注：即为数据结构中通常所说的时间复杂度）</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt">O(n)，或者说线形时间(linear-time)，表示了多种算法运行时间中的一种。其他还有O(log2 n)，O(n log 2 n)，O(n2)，O(2n)等等。我们无须关心这些繁杂的big-Oh记号，只需要知道在括号中的值越小，则代表数据结构的性能越好。举例来说，时间复杂度（在这里我还是觉得用时间复杂度比运行时间更能理解）为O(log n)的算法远比O(n)更有效率，因为log n<N。<BR></P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><BR>注：</P>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">我们需要温习以下数学知识。在这里，</SPAN><SPAN lang=EN-US>log <SUB>a</SUB> b</SPAN><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">另外一种表示方法为</SPAN><SPAN lang=EN-US>a<SUP>y</SUP>=b</SPAN><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">。因此，</SPAN><SPAN lang=EN-US>log<SUB>2</SUB>4=2</SPAN><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">，因为</SPAN><SPAN lang=EN-US>2<SUP>2</SUP>=4</SPAN><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">。</SPAN><SPAN lang=EN-US>Log<SUB>2</SUB>n</SPAN><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">增长速度比单个的</SPAN><SPAN lang=EN-US>n</SPAN><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">要慢得多，在第三部分我们将考察时间复杂度为</SPAN><SPAN lang=EN-US>O(log<SUB>2</SUB>n)</SPAN><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">的二叉树结构。（这个注释没多大意思啊！）</SPAN></P></FONT></SPAN>
<P class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><SPAN style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"></SPAN></P>
<P>在这篇系列文章中，我们将计算每一种新的数据结构和它们的渐进操作运行时间，并通过相似的操作比较其他数据结构在运行时间上的区别。</P>
<P>数组：一种线形的，可以直接访问的，单一数据结构</P>
<P>在程序编写中，数组是最简单也是最广泛使用的数据结构。在所有的程序语言中数组都具备以下共同的属性：<BR>1．数组的数据存储在一段连续的内存之中；<BR>2．数组的所有元素都必须是同一种数据类型，因此数组又被认为是单一数据结构(homogeneous data structures)；<BR>3．数组元素可以直接访问。（在很多数据结构中，这一特点是不必要的。例如，文章第四部分介绍的数据结构SkipList。要访问SkipList中的特定元素，你必须根据搜索其他元素直到找到搜索对象为止。然而对于数组而言，如果你知道你要查找第i个元素，就可以通过arrayName[i]来访问它。）（译注：很多语言都规定数组的下标从0开始，因此访问第i个元素，应为arrayName[i-1]）</P>
<P>以下是数组常用的操作：<BR>1．分配空间<BR>2．数据访问<BR>3．数组空间重分配（Redimensioning）</P>
<P>在C#里声明数组时，数组为空值（null）。下面的代码创建了一个名为booleanArray的数组变量，其值为空（null）：</P>
<P>Bool [] boolleanArray;</P>
<P>在使用该数组时，必须用一个特定数字给它分配空间，如下所示：</P>
<P>booleanArray = new bool[10];</P>
<P>通用的表述为：</P>
<P>arrayName = new arrayType[allocationSize];</P>
<P>它将在CLR托管堆里分配一块连续的内存空间，足以容纳数据类型为arrayTypes、个数为allocationSize的数组元素。如果arrayType为值类型（译注：如int类型），则有allocationSize个未封箱（unboxed）的arrayType值被创建。如果arrayType为引用类型(译注：如string类型)，则有allocationSize个arrayType引用类型值被创建。（如果你对值类型和引用类型、托管堆和栈之间的区别不熟悉，请查阅“理解.Net公共类型系统Common Type System”）</P>
<P>为帮助理解.Net Framework中数组的内部存储机制，请看下面的例子：</P>
<P>arrayName = new arrayType[allocationSize];</P>
<P>This allocates a contiguous block of memory in the CLR-managed heap large enough to hold the allocationSize number of arrayTypes. If arrayType is a value type, then allocationSize number of unboxed arrayType values are created. If arrayType is a reference type, then allocationSize number of arrayType references are created. (If you are unfamiliar with the difference between reference and value types and the managed heap versus the stack, check out Understanding .NET's Common Type System.)</P>
<P>To help hammer home how the .NET Framework stores the internals of an array, consider the following example:</P>
<P>bool [] booleanArray;<BR>FileInfo [] files;</P>
<P>booleanArray = new bool[10];<BR>files = new FileInfo[10];</P>
<P>这里，booleanArray是值类型System.Boolean数组，而files数组则是引用类型System.IO.FileInfo数组。图一显示了执行这四行代码后CLR托管堆的情况。<BR></P>
<P><SPAN lang=EN-US style="FONT-SIZE: 12pt; FONT-FAMILY: 'Times New Roman'; mso-font-kerning: 1.0pt; mso-fareast-font-family: 宋体; mso-ansi-language: EN-US; mso-fareast-language: ZH-CN; mso-bidi-language: AR-SA"><?xml:namespace prefix = v ns = "urn:schemas-microsoft-com:vml" /><v:shapetype id=_x0000_t75 coordsize="21600,21600" o:spt="75" o:preferrelative="t" path="m@4@5l@4@11@9@11@9@5xe" filled="f" stroked="f">&nbsp;<IMG height=220 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/1-1.gif" width=450 border=0><v:stroke joinstyle="miter"></v:stroke><v:formulas><v:f eqn="if lineDrawn pixelLineWidth 0"></v:f><v:f eqn="sum @0 1 0"></v:f><v:f eqn="sum 0 0 @1"></v:f><v:f eqn="prod @2 1 2"></v:f><v:f eqn="prod @3 21600 pixelWidth"></v:f><v:f eqn="prod @3 21600 pixelHeight"></v:f><v:f eqn="sum @0 0 1"></v:f><v:f eqn="prod @6 1 2"></v:f><v:f eqn="prod @7 21600 pixelWidth"></v:f><v:f eqn="sum @8 21600 0"></v:f><v:f eqn="prod @7 21600 pixelHeight"></v:f><v:f eqn="sum @10 21600 0"></v:f></v:formulas><v:path o:extrusionok="f" gradientshapeok="t" o:connecttype="rect"></v:path><o:lock v:ext="edit" aspectratio="t"></o:lock></v:shapetype></SPAN><BR>&nbsp;<BR>图一：在托管堆中顺序存放数组元素</P>
<P>请记住在files数组中存放的十个元素指向的是FileInfo实例。图二强调了这一点（hammers home this point，有些俚语的感觉，不知道怎么翻译），显示了如果我们为files数组中的FileInfo实例分配一些值后内存的分布情况。<BR>&nbsp;<BR></P>
<P><IMG height=188 src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/1-2.gif" width=450 border=0><BR>图二：在托管堆中顺序存放数组元素</P>
<P><BR>.Net中所有数组都支持对元素的读写操作。访问数组元素的语法格式如下：</P>
<P>// 读一个数组元素<BR>bool b = booleanArray[7];</P>
<P>// 写一个数组元素，即赋值<BR>booleanArray[0] = false;</P>
<P>访问一个数组元素的运行时间表示为O(1)，因为对它的访问时间是不变的。那就是说，不管数组存储了多少元素，查找一个元素所花的时间都是相同的。运行时间之所以不变，是因为数组元素是连续存放的，查找定位的时候只需要知道数组在内存中的起始位置，每个元素的大小，以及元素的索引值。</P>
<P>在托管代码中，数组的查找比实际的实现稍微复杂一些，因为在CLR中访问每个数组，都要确保索引值在其边界之内。如果数组索引超出边界，会抛出IndexOutOfRangeException异常。这种边界检查有助于确保我们在访问数组不至于意外地超出数组边界而进入另外一块内存区。而且它不会影响数组访问的时间，因为执行边界检查所需时间并不随数组元素的增加而增加。</P>
<P>注：如果数组元素特别多，索引边界检查会对应用程序的执行性能有稍许影响。而对于非托管代码，这种边界检查就被忽略了。要了解更多信息，请参考Jeffrey Richter所著的Applied Microsoft .NET Framework Programming第14章。</P>
<P>使用数组时，你也许需要改变数组大小。可以通过根据特定的长度大小创建一个新数组实例，并将旧数组的内容拷贝到新数组，来实现该操作。我们称这一过程为数组空间重分配(redimensioning)，如下代码：</P>
<P>using System;<BR>using System.Collections;</P>
<P>public class MyClass<BR>{<BR>&nbsp;&nbsp; public static void Main()<BR>&nbsp;&nbsp; {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 创建包含3个元素的int类型数组<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int [] fib = new int[3];<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fib[0] = 1;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fib[1] = 1;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fib[2] = 2;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 重新分配数组，长度为10<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int [] temp = new int[10];</P>
<P>// 将fib数组内容拷贝到临时数组<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fib.CopyTo(temp, 0);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 将临时数组赋给fib<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fib = temp;&nbsp;&nbsp; <BR>&nbsp;&nbsp; }<BR>}</P>
<P>在代码的最后一行，fib指向包含10个元素的Int32类型数组。Fib数组中3到9（译注：注意下标从0开始）的元素值默认为0（Int32类型）。</P>
<P>当我们要存储同种类型的数据（原文为heterogeneous types——异类数据类型，我怀疑有误）并仅需要直接访问数据时，数组是较好的数据结构。搜索未排序的数组时间复杂度是线形的。当我们对小型数组进行操作，或很少对它进行查询操作时，数组这种结构是可以接受的。但当你的应用程序需要存储大量数据，且频繁进行查询操作时，有很多其他数据结构更能适应你的工作。我们来看看本文接下来将要介绍的一些数据结构。（如果你要根据某个属性查找数组，且数组是根据该属性进行排序的，你可以使用二叉法（binary search）对其搜索，它的时间复杂度为O(log n)，与在二叉树中搜索的时间复杂度相同。事实上，数组类中包含了一个静态方法BinarySearch()。如要了解该方法的更多信息，请参考我早期的一篇文章“有效地搜索有序数组”。</P>
<P>注：.Net Framework同样支持多维数组。与一维数组一样，多维数组对数据元素的访问运行时间仍然是不变的。回想一下我们前面介绍的在n个元素的一维数组中查询操作的时间复杂度为O(n)。对于一个nxn的二维数组，时间复杂度为O(n2)，因为每次搜索都要检查n2个元素。以此类推，k维数组搜索的时间复杂度为O（nk）。</P>
<P>ArrayList：可存储不同类型数据、自增长的数组</P>
<P>明确地，数组在设计时受到一些限制，因为一维数组只能存储相同类型的数据，而且在使用数组时，必须为数组定义特定的长度。很多时候，开发人员要求数组更加灵活，它可以存储不同类型的数据，也不用去关心数组空间的分配。在.Net Framework基类库中提供了满足这样条件的数据结构——System.Collections.ArrayList。</P>
<P>如下的一小段代码是ArrayList的示例。注意到使用ArrayList时可以添加任意类型的数据，且不需要分配空间。所有的这些都由系统控制。</P>
<P>ArrayList countDown = new ArrayList();<BR>countDown.Add(5);<BR>countDown.Add(4);<BR>countDown.Add(3);<BR>countDown.Add(2);<BR>countDown.Add(1);<BR>countDown.Add("blast off!");<BR>countDown.Add(new ArrayList());</P>
<P>从深层次的含义来讲，ArrayList使用的存放类型为object的System.Array对象。既然所有类型都是直接或间接从object派生，自然一个object类型的数组也可以存放任何类型的元素。ArrayList默认创建16个object类型元素的数组，当然我们也可以通过构造函数中的参数或设置Capacity属性来定制ArrayList大小。通过Add()方法添加新元素，数组内部自动检查其容量。如果添加新元素导致越界，则容量则自动成倍增加，我们称为自增长。</P>
<P>ArrayList和Array一样，也可以通过索引直接访问：</P>
<P>// Read access<BR>int x = (int) countDown[0];<BR>string y = (string) countDown[5];</P>
<P>// Write access<BR>countDown[1] = 5;</P>
<P>// 会产生ArgumentOutOfRange 异常<BR>countDown[7] = 5;</P>
<P>既然ArrayList存储的是object类型的元素，因此从ArrayList中读元素时应该显示的指定类型转换。同时要注意的是，如果你访问的数组元素超过ArrayList的长度，系统会抛出System.ArgumentOutOfRange异常。</P>
<P>ArrayList提供了标准数组所不具备的自增长灵活性，但这种灵活性是以牺牲性能为代价的，尤其是当我们存储的是值类型——例如System.Int32，System.Double，System.Boolean等。它们在托管堆中是以未封箱形式(unboxed form)连续存放的。然而，ArrayList的内部机制是一个引用的object对象数组；因此，即使ArrayList中只存放了值类型，这些元素仍然会通过封箱（boxing）转换为引用类型。如图三所示：<BR>&nbsp;<IMG height=249 alt=1-3.gif src="http://wayfarer.cnblogs.com/images/cnblogs_com/wayfarer/1-3.gif" width=450 border=0></P>
<P>图三：存储连续块的object引用的ArrayList</P>
<P>在ArrayList中使用值类型，将额外进行封箱(boxing)和撤箱(unboxing)操作，当你的应用程序是一个很大的ArrayList，并频繁进行读写操作时，会很大程度上影响程序性能。如图3所示，对于引用类型而言，ArrayList和数组的内存分配是相同的。</P>
<P>比较数组而言，ArrayList的自增长并不会导致任何性能的下降。如果你知道存储到ArrayList的元素的准确数量，可以通过ArrayList构造函数初始化容量以关闭其自增长功能。而对于数组，当你不知道具体容量时，不得不在插入的数据元素超过数组长度的时候，手动改变数组的大小。</P>
<P>一个经典的计算机科学问题是：当程序运行时超出了缓存空间，应该分配多少新的空间为最佳。一种方案是是原来分配空间的基础上每次加1。例如数组最初分配了5个元素，那么在插入第6个元素之前，将其长度增加为6。显然，这种方案最大程度上节约了内存空间，但代价太大，因为每插入一个新元素都要进行一次再分配操作。</P>
<P>另一种方案刚好相反，也就是每次分配都在原来大小的基础上增加100倍。如果数组最初分配了5个元素，那么在插入第6个元素之前，数组空间增长为500。显然，该方案大大地减少了再分配操作的次数，但仅当插入极少的数据元素时，就会有上百的元素空间未使用，实在太浪费空间了！</P>
<P>ArrayList的渐近运行时间和标准数组一样。即使对ArrayList的操作是高开销的，尤其是存储值类型，其元素个数和每次操作的代价之间的关系与标准数组相同。<BR></P></DIV><img src ="http://www.cppblog.com/mzty/aggbug/2053.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/mzty/" target="_blank">梦在天涯</a> 2005-12-24 15:36 <a href="http://www.cppblog.com/mzty/archive/2005/12/24/2053.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>c＋＋单向链表  （讨论应不应该在默认的构造里就分配空间）</title><link>http://www.cppblog.com/mzty/archive/2005/10/28/870.html</link><dc:creator>梦在天涯</dc:creator><author>梦在天涯</author><pubDate>Fri, 28 Oct 2005 00:42:00 GMT</pubDate><guid>http://www.cppblog.com/mzty/archive/2005/10/28/870.html</guid><wfw:comment>http://www.cppblog.com/mzty/comments/870.html</wfw:comment><comments>http://www.cppblog.com/mzty/archive/2005/10/28/870.html#Feedback</comments><slash:comments>6</slash:comments><wfw:commentRss>http://www.cppblog.com/mzty/comments/commentRss/870.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/mzty/services/trackbacks/870.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: //				 IntLink.cpp : Defines the entry point for the console application.				//																								//				/**/										/**/								/**/										///////////////////////...&nbsp;&nbsp;<a href='http://www.cppblog.com/mzty/archive/2005/10/28/870.html'>阅读全文</a><img src ="http://www.cppblog.com/mzty/aggbug/870.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/mzty/" target="_blank">梦在天涯</a> 2005-10-28 08:42 <a href="http://www.cppblog.com/mzty/archive/2005/10/28/870.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>