<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>肉包子的摇滚生活 &#187; 算法</title>
	<atom:link href="http://www.fangyuqiang.com/archives/tag/%e7%ae%97%e6%b3%95/feed" rel="self" type="application/rss+xml" />
	<link>http://www.fangyuqiang.com</link>
	<description>前端开发，交互设计，用户体验</description>
	<lastBuildDate>Wed, 01 Sep 2010 09:06:13 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0.1</generator>
		<item>
		<title>Java实现RFC2865 Radius协议的密码加密</title>
		<link>http://www.fangyuqiang.com/archives/448</link>
		<comments>http://www.fangyuqiang.com/archives/448#comments</comments>
		<pubDate>Fri, 07 Aug 2009 02:20:18 +0000</pubDate>
		<dc:creator>fangyuqiang</dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[radius]]></category>
		<category><![CDATA[算法]]></category>

		<guid isPermaLink="false">http://www.fangyuqiang.com/?p=448</guid>
		<description><![CDATA[最近项目中要做Radius协议的客户端，一看Radius几百页的协议，我头皮都发麻。放狗搜索一番，发现了一个实现好的Java的Radius客户端，哈哈，很好很强大： http://sourceforge.net/projects/jradius-client/ 摸索了一番，挖出了里面关于用户密码加密的一段代码，相信对很多想学习下Java MD5加密的同学会很有帮助。 首先来了解一番背景跟算法要求： User-Password 描述 该属性表示用户用来认证的用户密码，或者是用户在收到接入挑战报文后用户的输入。它只出现在接入请求报文中。 在传输的过程中，密码是隐藏起来的，首先，将密码用null（\0）填充到16的整数倍个字节长，然后对在请求认证字后面加上共享密钥的字节流进行MD5加密生成hash值，将该hash值和密码的第一个16字节进行异或，然后将结果放入User-Password属性的第一个16字节中。 如果密码长度超过16个字符，则对第一个异或值后面加上共享密钥的字节流进行MD5加密生成hash值。该hash值和密码的第二个16个字节进行异或，然后将异或值放入User-Password属性的第二个16字节中。 如果必要，重复这个操作，每一个异或值后面加上共享密钥产生下一个hash值，然后和密码下一段的16字节进行异或。密码最长不能超过128个字符。 这个方法引用自Kaufman, Perlman和Speciner合写的《网络安全》一书，在第109到第110页。该方法的一个更加精确的解释如下： 共享密钥为S，伪随机数Request Authenticator为RA，将密码拆分成多个16个字节的块p1，p2，等等。最后一块用null（\0）填充满16个字节，密码块为c(1)，c(2)，等等。中间值为b1，b2，等等。 b1 = MD5(S + RA) c(1) = p1 xor b1 b2 = MD5(S + c(1)) c(2) = p2 xor b2 . . . . . . bi = MD5(S + c(i-1)) c(i) = pi xor bi User-Password属性字符串为c(1)+c(2)+&#8230;c(i)，+表示连接。 一旦接收到报文，逆向处理就能得到密码明文。 User-Password属性的格式如下所示。各个域是按照自左向右的顺序传输的。 0 [...]]]></description>
			<content:encoded><![CDATA[<p>最近项目中要做Radius协议的客户端，一看Radius几百页的协议，我头皮都发麻。放狗搜索一番，发现了一个实现好的Java的Radius客户端，哈哈，很好很强大：</p>
<p><a href="http://sourceforge.net/projects/jradius-client/" target="_blank">http://sourceforge.net/projects/jradius-client/ </a></p>
<p>摸索了一番，挖出了里面关于用户密码加密的一段代码，相信对很多想学习下Java MD5加密的同学会很有帮助。</p>
<p><span id="more-448"></span>首先来了解一番背景跟算法要求：</p>
<p>User-Password</p>
<p><strong>描述</strong></p>
<p>该属性表示用户用来认证的用户密码，或者是用户在收到接入挑战报文后用户的输入。它只出现在接入请求报文中。</p>
<p>在传输的过程中，密码是隐藏起来的，首先，将密码用null（\0）填充到16的整数倍个字节长，然后对在请求认证字后面加上共享密钥的字节流进行MD5加密生成hash值，将该hash值和密码的第一个16字节进行异或，然后将结果放入User-Password属性的第一个16字节中。</p>
<p>如果密码长度超过16个字符，则对第一个异或值后面加上共享密钥的字节流进行MD5加密生成hash值。该hash值和密码的第二个16个字节进行异或，然后将异或值放入User-Password属性的第二个16字节中。</p>
<p>如果必要，重复这个操作，每一个异或值后面加上共享密钥产生下一个hash值，然后和密码下一段的16字节进行异或。密码最长不能超过128个字符。</p>
<p>这个方法引用自Kaufman, Perlman和Speciner合写的《网络安全》一书，在第109到第110页。该方法的一个更加精确的解释如下：</p>
<blockquote><p>共享密钥为S，伪随机数Request Authenticator为RA，将密码拆分成多个16个字节的块p1，p2，等等。最后一块用null（\0）填充满16个字节，密码块为c(1)，c(2)，等等。中间值为b1，b2，等等。</p>
<p>b1 = MD5(S + RA)       c(1) = p1 xor b1<br />
b2 = MD5(S + c(1))     c(2) = p2 xor b2<br />
.                       .<br />
.                       .<br />
.                       .<br />
bi = MD5(S + c(i-1))   c(i) = pi xor bi<br />
User-Password属性字符串为c(1)+c(2)+&#8230;c(i)，+表示连接。<br />
一旦接收到报文，逆向处理就能得到密码明文。</p></blockquote>
<p>User-Password属性的格式如下所示。各个域是按照自左向右的顺序传输的。</p>
<pre>    0              1               2
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
   |     Type      |    Length     |  String ...
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-</pre>
<p><strong>类型</strong></p>
<p>2               代表用户密码</p>
<p><strong>长度</strong></p>
<p>至少18字节，但不能超过130个字节</p>
<p><strong>字符串</strong></p>
<p>本字符串占位16到128（包含）个字节。</p>
<h3>Java RADIUS Client的密码生成实现</h3>
<p>//生成请求的Authenticator</p>
<pre title="code" class="java">byte [] requestAuthenticator = this.makeRFC2865RequestAuthenticator();

//Authenticator的生成
private byte[] makeRFC2865RequestAuthenticator() {
 byte [] requestAuthenticator = new byte[16];

 Random r = new Random();

 for (int i = 0; i &lt; 16; i++)
 {
 requestAuthenticator[i] = (byte) r.nextInt();
 }

 this.md5MessageDigest.reset();
 this.md5MessageDigest.update(this.sharedSecret.getBytes());//sharedSecret指的是服务器端与客户端的共享密钥
 this.md5MessageDigest.update(requestAuthenticator);

 return this.md5MessageDigest.digest();//返回生成的hash值
 }
//生成加密后的密钥，userPass指未加密的密码
byte [] encryptedPass = this.encodePapPassword(userPass, requestAuthenticator);

//用来加密的算法
	private byte[] encodePapPassword(final byte[] userPass,
			final byte[] requestAuthenticator) {
		// encrypt the password.
		byte[] userPassBytes = null;
//密码必须大于16小于或者等于128bytes,如果不是16的整数倍个字节长，不足的位补0。如果大于128位，则截断成128位。

		if (userPass.length > 128) {
			userPassBytes = new byte[128];
			System.arraycopy(userPass, 0, userPassBytes, 0, 128);
		} else {
			userPassBytes = userPass;
		}
		// declare the byte array to hold the final product
		byte[] encryptedPass = null;

		if (userPassBytes.length < 128) {
			if (userPassBytes.length % 16 == 0) {
				 // 如果是16的整数倍长
				encryptedPass = new byte[userPassBytes.length];
			} else {
				// 数组长度变成16位的整数倍长
				encryptedPass = new byte[((userPassBytes.length / 16) * 16) + 16];
			}
		} else {
			// the encrypted password must be between 16 and 128 bytes
			encryptedPass = new byte[128];
		}

		 // 填充0
		System.arraycopy(userPassBytes, 0, encryptedPass, 0,
				userPassBytes.length);
		for (int i = userPassBytes.length; i < encryptedPass.length; i++) {
			encryptedPass[i] = 0; // fill it out with zeroes
		}
		this.md5MessageDigest.reset();
		 // 添加共享密钥
		this.md5MessageDigest.update(this.sharedSecret.getBytes());
		 // 添加请求Authenticator
		this.md5MessageDigest.update(requestAuthenticator);
		//  //获得md5 hash( b1 = MD5(S + RA) ).
		byte bn[] = this.md5MessageDigest.digest();

		for (int i = 0; i < 16; i++) {
			// perform the XOR as specified by RFC 2865.
			encryptedPass[i] = (byte) (bn[i] ^ encryptedPass[i]);
		}

		if (encryptedPass.length > 16) {
			for (int i = 16; i < encryptedPass.length; i += 16) {
				this.md5MessageDigest.reset();
				// add the shared secret
				this.md5MessageDigest.update(this.sharedSecret.getBytes());
				// add the previous(encrypted) 16 bytes of the user password
				this.md5MessageDigest.update(encryptedPass, i - 16, 16);
				// get the md5 hash( bn = MD5(S + c(i-1)) ).
				bn = this.md5MessageDigest.digest();
				for (int j = 0; j < 16; j++) {
					// perform the XOR as specified by RFC 2865.
					encryptedPass[i + j] = (byte) (bn[j] ^ encryptedPass[i + j]);
				}
			}
		}
		return encryptedPass;
	}
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.fangyuqiang.com/archives/448/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>优化JavaScript脚本的性能总结</title>
		<link>http://www.fangyuqiang.com/archives/417</link>
		<comments>http://www.fangyuqiang.com/archives/417#comments</comments>
		<pubDate>Fri, 31 Jul 2009 15:48:49 +0000</pubDate>
		<dc:creator>fangyuqiang</dc:creator>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[算法]]></category>

		<guid isPermaLink="false">http://www.fangyuqiang.com/?p=417</guid>
		<description><![CDATA[接下来博客的文章，打算结合梅花雪的文章，以及曹力等人的一些文章，来系统的介绍下如何对JavaScript脚本做性能优化。 JavaScript性能优化重要吗？当然，如果你的JavaScript只是做一些简单的表单验证，你几乎可以忽略。但是，如果你是构建一个复杂的基于JavaScript的程序，例如webgame，富客户端，那么优化JavaScript程序的性能就很重要了。 瓶颈在哪里？ 如果你是急匆匆的搜索到这篇文章，觉得你的js太缓慢了，可以先看看下面几个tips，或许对你有帮助： IE6，7的JavaScript解释器非常差，比起firefox，opera，chrome，safari等浏览器，差距是数量级的。相反，这几个浏览器的JavaScript效率都非常的高，如果你的js在这几个浏览器下面也很慢，才真正有问题。而事实上，对JavaScript做优化，目前而言都是在对IE6，7做优化，而IE8的js效率已经提升了很多，跟firefox等没有数量级上的差距了，但是性能还是落后的。 检查你代码里面的循环，递归调用等地方的代码，是否存在逻辑问题？或者说业务本身就这么复杂需要长时间的js执行？ 检查代码里面的字符串拼接操作，IE6，7的字符串拼接，应该说是其JavaScript解释器的错误实现，导致IE6，7在处理字符串拼接上存在严重的效率问题，具体的解决方法可以参看下面的详细介绍。 检查页面DOM元素的数量。页面DOM元素过多,会导致浏览器JS运行和DOM渲染效率降低，这往往是一个根本性的原因，采用ext做出来的系统，很多情况下会出现这个问题，而这个问题，目前而言并没有什么最佳的解决方案。如果ext是选定的架构，那么应该在DOM元素数量的控制上下一番功能。 （51js论坛上有人提出的）过于频繁的DOM操作同样会使得页面速度变得缓慢。例如在拖动效果中，如果拖动容器内的结点数太多，则在拖动过程中出现很卡的现象——在整个拖动过程中，浏览器要不断重新宣染容器及其内部所有结点的相关属性，任务量大，触发又很频繁，CPU占用就会很高。改进这样的拖动效果，可以改成：按下鼠标时，是拖动一个独立的空DIV，松开鼠标时再把真正要移动的窗口重新定位到目标位置就行。这样在拖动过程中，浏览器只需要宣染一个div。 以下我们再结合曹力的如何优化JavaScript脚本的性能一文，来具体的分析优化的具体实现。曹力从多种角度分析了JavaScript脚本可以优化的地方。不过，个人认为，以上提到的几个tips才往往是解决的关键，除此之外的，曹力在文章中提到的一些优化实际操作中意义并不大，因为很多优化往往是需要到一个很高的数量级才会有明显的效果，所以在此我顺便点评了优化指数，即做这个优化的必要性，5星最高，3星程度就是基本一定要做的了。 语言层次方面 循环 循环是很常用的一个控制结构，大部分东西要依靠它来完成，在JavaScript中，我们可以使用for(;;),while(),for(in)三种循环，事实上，这三种循环中for(in)的效率极差，因为他需要查询散列键，只要可以就应该尽量少用。for(;;)和while循环的性能应该说基本（平时使用时）等价。 而事实上，如何使用这两个循环，则有很大讲究。我在测试中有些很有意思的情况，见附录。最后得出的结论是： 如果是循环变量递增或递减，不要单独对循环变量赋值，应该在它最后一次读取的时候使用嵌套的++或&#8211;操作符。 如果要与数组的长度作比较，应该事先把数组的length属性放入一个局部变量中，减少查询次数。 举例，假设arr是一个数组，最佳的遍历元素方式为： for(var i=0, len = arr.length;i0;i--){...} 优化指数：1星。除非你循环的次数达到万以上的数量级，否则这样的优化并不能带来明显的收益，不过养成这样的写法是个好习惯。for(in)的效率有多差这个曹力没给出测试结果，但是我认为该怎么用还是怎么用，前提还是循环的次数，在jQuery之类的框架源码里面都有很多的for(in)的使用。 局部变量和全局变量 局部变量的速度要比全局变量的访问速度更快，因为全局变量其实是全局对象的成员，而局部变量是放在函数的栈当中的。 优化指数：2星。将全局变量改成局部变量提高的性能也并不明显，但是合理设计程序的结构，减少全局变量的使用是比较良好的编程习惯，可以避免一些无意中带来的错误或者bug。 不使用Eval 使用eval相当于在运行时再次调用解释引擎对内容进行运行，需要消耗大量时间。这时候使用JavaScript所支持的闭包可以实现函数模版（关于闭包的内容请参考函数式编程的有关内容） 优化指数：3星。关于eval我还没有进行过深入的测试，不过根据多个资料的介绍，eval确实会消耗大量时间，不适宜在程序中多使用。此外，with语句也一样，不适宜多使用。 减少对象查找 因为JavaScript的解释性，所以a.b.c.d.e，需要进行至少4次查询操作，先检查a再检查a中的b，再检查b中的c，如此往下。所以如果这样的表达式重复出现，只要可能，应该尽量少出现这样的表达式，可以利用局部变量，把它放入一个临时的地方进行查询。 这一点可以和循环结合起来，因为我们常常要根据字符串、数组的长度进行循环，而通常这个长度是不变的，比如每次查询a.length，就要额外进行一个操作，而预先把var len=a.length，则就少了一次查询。 优化指数：1星。一样，减少对象查找这样提升的效率也很不明显，一样还是要到一个数量级才行。比如YUI这样的，重度的命名空间，都是XX.XX.XX.XX，但它一样应用得很广泛。 字符串连接 如果是追加字符串，最好使用s+=anotherStr操作，而不是要使用s=s+anotherStr。 如果要连接多个字符串，应该少使用+=，如 s+=a; s+=b; s+=c; 应该写成 s+=a + b + c； 而如果是收集字符串，比如多次对同一个字符串进行+=操作的话，最好使用一个缓存。怎么用呢？使用JavaScript数组来收集，最后使用join方法连接起来，如下 var buf = new Array(); for(var i = [...]]]></description>
			<content:encoded><![CDATA[<p>接下来博客的文章，打算结合梅花雪的文章，以及曹力等人的一些文章，来系统的介绍下如何对JavaScript脚本做性能优化。</p>
<p>JavaScript性能优化重要吗？当然，如果你的JavaScript只是做一些简单的表单验证，你几乎可以忽略。但是，如果你是构建一个复杂的基于JavaScript的程序，例如webgame，富客户端，那么优化JavaScript程序的性能就很重要了。<span id="more-417"></span></p>
<h2>瓶颈在哪里？</h2>
<p>如果你是急匆匆的搜索到这篇文章，觉得你的js太缓慢了，可以先看看下面几个tips，或许对你有帮助：</p>
<ul>
<li><strong>IE6，7的JavaScript解释器非常差，比起firefox，opera，chrome，safari等浏览器，差距是数量级的</strong>。相反，这几个浏览器的JavaScript效率都非常的高，如果你的js在这几个浏览器下面也很慢，才真正有问题。而事实上，<strong>对JavaScript做优化，目前而言都是在对IE6，7做优化</strong>，而IE8的js效率已经提升了很多，跟firefox等没有数量级上的差距了，但是性能还是落后的。</li>
<li>检查你代码里面的<strong>循环，递归调用</strong>等地方的代码，是否存在逻辑问题？或者说业务本身就这么复杂需要长时间的js执行？</li>
<li>检查代码里面的<strong>字符串拼接操作</strong>，IE6，7的字符串拼接，应该说是其JavaScript解释器的错误实现，导致IE6，7在处理字符串拼接上存在严重的效率问题，具体的解决方法可以参看下面的详细介绍。</li>
<li>检查页面DOM元素的数量。<strong>页面DOM元素过多,会导致浏览器JS运行和DOM渲染效率降低</strong>，这往往是一个根本性的原因，采用ext做出来的系统，很多情况下会出现这个问题，而这个问题，目前而言并没有什么最佳的解决方案。如果ext是选定的架构，那么应该在DOM元素数量的控制上下一番功能。</li>
<li>（51js论坛上有人提出的）<strong>过于频繁的DOM操作同样会使得页面速度变得缓慢</strong>。例如在拖动效果中，如果拖动容器内的结点数太多，则在拖动过程中出现很卡的现象——<strong>在整个拖动过程中，浏览器要不断重新宣染容器及其内部所有结点的相关属性，任务量大，触发又很频繁</strong>，CPU占用就会很高。改进这样的拖动效果，可以改成：按下鼠标时，是拖动一个独立的空DIV，松开鼠标时再把真正要移动的窗口重新定位到目标位置就行。这样在拖动过程中，浏览器只需要宣染一个div。</li>
</ul>
<p>以下我们再结合曹力的<a href="http://shiningray.cn/improve-javascript-performance.html">如何优化JavaScript脚本的性能</a>一文，来具体的分析优化的具体实现。曹力从多种角度分析了JavaScript脚本可以优化的地方。不过，个人认为，<strong>以上提到的几个tips才往往是解决的关键，除此之外的，曹力在文章中提到的一些优化实际操作中意义并不大，因为很多优化往往是需要到一个很高的数量级才会有明显的效果</strong>，所以在此我顺便点评了优化指数，即做这个优化的必要性，5星最高，3星程度就是基本一定要做的了。</p>
<h2>语言层次方面</h2>
<h3>循环</h3>
<p>循环是很常用的一个控制结构，大部分东西要依靠它来完成，在JavaScript中，我们可以使用for(;;),while(),for(in)三种循环，事实上，这三种循环中for(in)的效率极差，因为他需要查询散列键，只要可以就应该尽量少用。for(;;)和while循环的性能应该说基本（平时使用时）等价。</p>
<p>而事实上，如何使用这两个循环，则有很大讲究。我在测试中有些很有意思的情况，见附录。最后得出的结论是：</p>
<ul>
<li>如果是循环变量递增或递减，不要单独对循环变量赋值，应该在它最后一次读取的时候使用嵌套的++或&#8211;操作符。</li>
<li>如果要与数组的长度作比较，应该事先把数组的length属性放入一个局部变量中，减少查询次数。</li>
</ul>
<p>举例，假设arr是一个数组，最佳的遍历元素方式为：</p>
<pre>for(var i=0, len = arr.length;i0;i--){...}</pre>
<p>优化指数：1星。<strong>除非你循环的次数达到万以上的数量级，否则这样的优化并不能带来明显的收益，不过养成这样的写法是个好习惯</strong>。for(in)的效率有多差这个曹力没给出测试结果，但是我认为该怎么用还是怎么用，前提还是循环的次数，在jQuery之类的框架源码里面都有很多的for(in)的使用。</p>
<h3>局部变量和全局变量</h3>
<p>局部变量的速度要比全局变量的访问速度更快，因为全局变量其实是全局对象的成员，而局部变量是放在函数的栈当中的。</p>
<p>优化指数：2星。将全局变量改成局部变量提高的性能也并不明显，但是<strong>合理设计程序的结构，减少全局变量的使用是比较良好的编程习惯，可以避免一些无意中带来的错误或者bug</strong>。</p>
<h3>不使用Eval</h3>
<p>使用eval相当于在运行时再次调用解释引擎对内容进行运行，需要消耗大量时间。这时候使用JavaScript所支持的闭包可以实现函数模版（关于闭包的内容请参考函数式编程的有关内容）</p>
<p>优化指数：3星。关于eval我还没有进行过深入的测试，不过根据多个资料的介绍，<strong>eval确实会消耗大量时间，不适宜在程序中多使用。此外，with语句也一样，不适宜多使用</strong>。</p>
<h3>减少对象查找</h3>
<p>因为JavaScript的解释性，所以a.b.c.d.e，需要进行至少4次查询操作，先检查a再检查a中的b，再检查b中的c，如此往下。所以如果这样的表达式重复出现，只要可能，应该尽量少出现这样的表达式，可以利用局部变量，把它放入一个临时的地方进行查询。</p>
<p>这一点可以和循环结合起来，因为我们常常要根据字符串、数组的长度进行循环，而通常这个长度是不变的，比如每次查询a.length，就要额外进行一个操作，而预先把var len=a.length，则就少了一次查询。</p>
<p>优化指数：1星。一样，<strong>减少对象查找这样提升的效率也很不明显，一样还是要到一个数量级才行</strong>。比如YUI这样的，重度的命名空间，都是XX.XX.XX.XX，但它一样应用得很广泛。</p>
<h3>字符串连接</h3>
<p>如果是追加字符串，最好使用s+=anotherStr操作，而不是要使用s=s+anotherStr。</p>
<p>如果要连接多个字符串，应该少使用+=，如</p>
<pre>s+=a;
s+=b;
s+=c;</pre>
<p>应该写成</p>
<pre>s+=a + b + c；</pre>
<p>而如果是收集字符串，比如多次对同一个字符串进行+=操作的话，最好使用一个缓存。怎么用呢？使用JavaScript数组来收集，最后使用join方法连接起来，如下</p>
<pre>var buf = new Array();
for(var i = 0; i &lt; 100; i++){ 	

buf.push(i.toString()); 

} 

var all = buf.join("");</pre>
<p>优化指数：4星。如果你的js程序经常涉及到字符串的拼接操作的话，这个优化的意义就很重大了。当然，这个优化在ff，chrome之类的浏览器上面并没有什么明显的效果。但是别忘了有个浏览器叫IE。<strong>ie6跟ie7下面，字符串拼接的引擎实现有错误，所以在大量字符串拼接的情况下，用数组的join操作来拼接字符串会比用+号来拼接提升一个数量级效率</strong>。</p>
<h3>类型转换</h3>
<p>类型转换是大家常犯的错误，因为JavaScript是动态类型语言，你不能指定变量的类型。</p>
<p>1. 把数字转换成字符串，应用&#8221;" + 1，虽然看起来比较丑一点，但事实上这个效率是最高的，性能上来说：</p>
<p>(&#8220;&#8221; + ) &gt; String() &gt; .toString() &gt; new String()</p>
<p>这条其实和下面的“直接量”有点类似，尽量使用编译时就能使用的内部操作要比运行时使用的用户操作要快。</p>
<p>String()属于内部函数，所以速度很快，而.toString()要查询原型中的函数，所以速度逊色一些，new String()用于返回一个精确的副本。</p>
<p>2.浮点数转换成整型，这个更容易出错，很多人喜欢使用parseInt()，其实parseInt()是用于将字符串转换成数字，而不是浮点数和整型之间的转换，我们应该使用Math.floor()或者Math.round()。</p>
<p>另外，和第二节的对象查找中的问题不一样，Math是内部对象，所以Math.floor()其实并没有多少查询方法和调用的时间，速度是最快的。</p>
<p>3.对于自定义的对象，如果定义了toString()方法来进行类型转换的话，推荐显式调用toString()，因为内部的操作在尝试所有可能性之后，会尝试对象的toString()方法尝试能否转化为String，所以直接调用这个方法效率会更高</p>
<p>优化指数：2.5星。这些优化一样是没办法得到多大的提升的，但是养成这样的写法与做法确实很有必要的。很多新手与老手的差距也都体现在对细节的处理上面。</p>
<h3>使用直接量</h3>
<p>其实这个影响倒比较小，可以忽略。什么叫使用直接量，比如，JavaScript支持使用[param,param,param,...]来直接表达一个数组，以往我们都使用new Array(param,param,&#8230;)，使用前者是引擎直接解释的，后者要调用一个Array内部构造器，所以要略微快一点点。</p>
<p>同样，var foo = {}的方式也比var foo = new Object();快，var reg = /../;要比var reg=new RegExp()快。</p>
<p>优化指数：1星。使用直接量是现在很流行的做法，建议写法上养成一样的习惯，代码看起来会更清晰。</p>
<h3>字符串遍历操作</h3>
<p>对字符串进行循环操作，譬如替换、查找，应使用正则表达式，因为本身JavaScript的循环速度就比较慢，而正则表达式的操作是用C写成的语言的API，性能很好。</p>
<p>优化指数：1.5星。优化是有前提的，看你需要操作的字符串有多大，普通的字符串，该用什么方式还是用什么方式。</p>
<h3>高级对象</h3>
<p>自定义高级对象和Date、RegExp对象在构造时都会消耗大量时间。如果可以复用，应采用缓存的方式。</p>
<p>优化指数：2星。</p>
<h2>DOM相关</h2>
<h3>插入HTML</h3>
<p>很多人喜欢在JavaScript中使用document.write来给页面生成内容。事实上这样的效率较低，如果需要直接插入HTML，可以找一个容器元素，比如指定一个div或者span，并设置他们的innerHTML来将自己的HTML代码插入到页面中。</p>
<p>优化指数：3星。document.write虽然方便，但是不仅效率较低，同样也带来一些维护上困难。在你经常需要设置innerHTML的情况下，那就更不要去使用document.write。</p>
<h3>对象查询</h3>
<p>使用[""]查询要比.items()更快，这和前面的减少对象查找的思路是一样的，调用.items()增加了一次查询和函数的调用。</p>
<p>优化指数：2星。使用[""]提升的性能可以忽略，不过我觉得[""]的写法会更加清晰，此外，<strong>item()这个的方法只有IE下才能用，firefox下面是不行的。</strong></p>
<h3>创建DOM节点</h3>
<p>通常我们可能会使用字符串直接写HTML来创建节点，其实这样做</p>
<p>1.无法保证代码的有效性</p>
<p>2.字符串操作效率低</p>
<p>所以应该是用document.createElement()方法，而如果文档中存在现成的样板节点，应该是用cloneNode()方法，因为使用createElement()方法之后，你需要设置多次元素的属性，使用cloneNode()则可以减少属性的设置次数——同样如果需要创建很多元素，应该先准备一个样板节点。</p>
<p>优化指数：2星。同样的，直接用HTML方式来创建节点还是很多便利的，而createElement()的方式则要写不少代码。这性能跟便利之间，取舍其实完全在你自己了。</p>
<h3>定时器</h3>
<p>如果针对的是不断运行的代码，不应该使用setTimeout，而应该是用setInterval。setTimeout每次要重新设置一个定时器。</p>
<p>优化指数：1星。本身这2个定时器使用的场合就不太一样。很多setTimeout的场合用setInterval并不适合。</p>
<h2>其他</h2>
<h3>脚本引擎</h3>
<p>据我测试Microsoft的JScript的效率较Mozilla的Spidermonkey要差很多，无论是执行速度还是内存管理上，因为JScript现在基本也不更新了。但SpiderMonkey不能使用ActiveXObject</p>
<h3>文件优化</h3>
<p>文件优化也是一个很有效的手段，删除所有的空格和注释，把代码放入一行内，可以加快下载的速度，注意，是下载的速度而不是解析的速度，如果是本地，注释和空格并不会影响解释和执行速度。</p>
<p>优化指数：5星。产品化的代码，用压缩工具处理后再放出是必须的。可用的工具很多，比如YUI Compressor，JS min。虽然它并不是提高程序的性能，但是<strong>JavaScript作为客户端执行的程序，更快下载到客户端就能更早得到执行，这个时间往往比你费尽心思的优化提高得多得多</strong>。想象一下，如果extjs没有压缩，那么客户端需要多么漫长的等待才能看到界面。</p>
<h2>曹力的总结</h2>
<p>本文总结了我在JavaScript编程中所找到的提高JavaScript运行性能的一些方法，其实这些经验都基于几条原则：</p>
<p>1.直接拿手头现成的东西比较快，如局部变量比全局变量快，直接量比运行时构造对象快等等。</p>
<p>2.尽可能少地减少执行次数，比如先缓存需要多次查询的。</p>
<p>3.尽可能使用语言内置的功能，比如串链接。</p>
<p>4.尽可能使用系统提供的API，因为这些API是编译好的二进制代码，执行效率很高</p>
<p>同时，一些基本的算法上的优化，同样可以用在JavaScript中，比如运算结构的调整，这里就不再赘述了。但是由于JavaScript是解释型的，一般不会在运行时对字节码进行优化，所以这些优化仍然是很重要的。</p>
<p>当然，其实这里的一些技巧同样使用在其他的一些解释型语言中，大家也可以进行参考。</p>
<h2>参考</h2>
<ul>
<li><a href="http://www.umsu.de/jsperf/ 各种浏览器的测试对比">http://www.umsu.de/jsperf/ 各种浏览器的测试对比</a></li>
<li><a href="http://home.earthlink.net/~kendrasg/info/js_opt/">http://home.earthlink.net/~kendrasg/info/js_opt/</a></li>
<li><a href="http://shiningray.cn/improve-javascript-performance.html">Shining Ray » 如何优化JavaScript脚本的性能</a>.</li>
</ul>
<div id="_mcePaste" style="overflow: hidden; position: absolute; left: -10000px; top: 2430px; width: 1px; height: 1px;">
<h1>附录1</h1>
<p>由于是以前做过的测试，测试代码已经不全，我补充了一部分如下：</p>
<pre class="js" title="code">var print;

if(typeof document != "undefined" ){
    print = function(){
		document.write(arguments[0]);
	}
}else if(typeof WScript != "undefined" ){
    print = function(){
        WScript.Echo(arguments[0],arguments[1],arguments[2]);
    }
}

function empty(){
}

function benchmark(f){
    var i = 0;
    var start = (new Date()).getTime();

    while(i &lt; pressure){
        f(i++);
    }
    var end = (new Date()).getTime();
    WScript.Echo(end-start);
}

/*
i=0
start = (new Date()).getTime();
while(i &lt; 60000){
    c = [i,i,i,i,i,i,i,i,i,i];
    i++;
}
end = (new Date()).getTime();
WScript.Echo(end-start);
i=0
start = (new Date()).getTime();
while(i &lt; 60000){
    c = new Array(i,i,i,i,i,i,i,i,i,i);
    i++;
}
var end = (new Date()).getTime();
WScript.Echo(end-start);
*/

function internCast(i){
    return "" + i;
}

function StringCast(i){
    return String(i)
}
function newStringCast(i){
    return new String(i)
}
function toStringCast(i){
    return i.toString();
}
function ParseInt(){
    return parseInt(j);
}
function MathFloor(){
    return Math.floor(j);
}
function Floor(){
    return floor(j);
}
var pressure = 50000;
var a  = "";
var floor = Math.floor;
j = 123.123;
print("-------------\nString Conversion Test");
print("The empty:", benchmark(empty));
print("intern:", benchmark(internCast));
print("String:");
benchmark(StringCast);
print("new String:");
benchmark(newStringCast);
print("toString:");
benchmark(toStringCast);
print("-------------\nFloat to Int Conversion Test");
print("parseInt");
benchmark(ParseInt);
print("Math.floor");
benchmark(MathFloor);
print("floor")
benchmark(Floor);

function newObject(){
    return new Object();
}

function internObject(){
    return {};
}
print("------------\nliteral Test");
print("runtime new object", benchmark(newObject));
print("literal object", benchmark(internObject));</pre>
<h1>附录2</h1>
<p>代码1：</p>
<pre class="js" title="code">    for(var i=0;i&lt;100;i++){
        arr[i]=0;
    }</pre>
<p>代码2：</p>
<pre class="js" title="code">    var i = 0;
    while(i &lt; 100){
        arr[i++]=0;
    }</pre>
<p>代码3：</p>
<pre class="js" title="code">    var i = 0;
    while(i &lt; 100){
        arr[i]=0;
        i++;
    }</pre>
<p>在firefox下测试这两段代码，结果是代码2优于代码1和3，而代码1一般优于代码3，有时会被代码3超过；而在IE</p>
<p>6.0下，测试压力较大的时候（如测试10000次以上）代码2和3则有时候优于代码1，有时候就会远远落后代码1，而在测试压力较小（如5000次），则代码2&gt;代码3&gt;代码1。</p>
<p>代码4：</p>
<pre class="js" title="code">    var i = 0;
    var a;
    while(i &lt; 100){
        a = 0;
        i++;
    }</pre>
<p>代码5：</p>
<pre class="js" title="code">    var a;
    for(var i=0;i&lt;100;i++){
        a = 0;
    }</pre>
<p>上面两段代码在Firefox和IE下测试结果都是性能接近的。</p>
<p>代码6：</p>
<pre class="js" title="code">    var a;
    var i=0;
    while(i&lt;100){
        a=i;
        i++;
    }</pre>
<p>代码7：</p>
<pre class="js" title="code">    var a;
    var i=0;
    while(i&lt;100){
        a=i++;
    }</pre>
<p>代码8：</p>
<pre class="js" title="code">    var a;
    for(var i=0;i&lt;100;i++){
        a = i;
    }</pre>
<p>代码9：</p>
<pre class="js" title="code">    var a;
    for(var i=0;i&lt;100;){
        a = i++;
    }</pre>
<p>这四段代码在Firefox下6和8的性能接近，7和9的性能接近，而6,</p>
<p>8 &lt; 7, 9；</p>
<p>最后我们来看一下空循环</p>
<p>代码10：</p>
<pre class="js" title="code">for(var i=0;i&lt;100;i++){   

}</pre>
<p>代码11：</p>
<pre class="js" title="code">var i;
 while(i&lt;100){
 i++;
}</pre>
<p>最后的测试出现了神奇的结果，Firefox下代码10所花的时间与代码11所花的大约是24:1。所以它不具备参考价值，于是我没有放在一开始给大家看。</p></div>
<h2>附录1</h2>
<p>由于是以前做过的测试，测试代码已经不全，我补充了一部分如下：</p>
<pre class="js" title="code">var print;
if(typeof document != "undefined" ){
print = function(){
document.write(arguments[0]);
}
}else if(typeof WScript != "undefined" ){
print = function(){
WScript.Echo(arguments[0],arguments[1],arguments[2]);
}
}
function empty(){
}
function benchmark(f){
var i = 0;
var start = (new Date()).getTime();
while(i &lt; pressure){
f(i++);
}
var end = (new Date()).getTime();
WScript.Echo(end-start);
}
/*
i=0
start = (new Date()).getTime();
while(i &lt; 60000){
c = [i,i,i,i,i,i,i,i,i,i];
i++;
}
end = (new Date()).getTime();
WScript.Echo(end-start);
i=0
start = (new Date()).getTime();
while(i &lt; 60000){
c = new Array(i,i,i,i,i,i,i,i,i,i);
i++;
}
var end = (new Date()).getTime();
WScript.Echo(end-start);
*/
function internCast(i){
return "" + i;
}
function StringCast(i){
return String(i)
}
function newStringCast(i){
return new String(i)
}
function toStringCast(i){
return i.toString();
}
function ParseInt(){
return parseInt(j);
}
function MathFloor(){
return Math.floor(j);
}
function Floor(){
return floor(j);
}
var pressure = 50000;
var a  = "";
var floor = Math.floor;
j = 123.123;
print("-------------\nString Conversion Test");
print("The empty:", benchmark(empty));
print("intern:", benchmark(internCast));
print("String:");
benchmark(StringCast);
print("new String:");
benchmark(newStringCast);
print("toString:");
benchmark(toStringCast);
print("-------------\nFloat to Int Conversion Test");
print("parseInt");
benchmark(ParseInt);
print("Math.floor");
benchmark(MathFloor);
print("floor")
benchmark(Floor);
function newObject(){
return new Object();
}
function internObject(){
return {};
}
print("------------\nliteral Test");
print("runtime new object", benchmark(newObject));
print("literal object", benchmark(internObject));</pre>
<h2>附录2</h2>
<h3>代码1：</h3>
<pre class="js" title="code">for(var i=0;i&lt;100;i++){
arr[i]=0;
}</pre>
<h3>代码2：</h3>
<pre class="js" title="code">var i = 0;
while(i &lt; 100){
arr[i++]=0;
}</pre>
<h3>代码3：</h3>
<pre class="js" title="code">var i = 0;
while(i &lt; 100){
arr[i]=0;
 i++;
 }</pre>
<p>在firefox下测试这两段代码，结果是代码2优于代码1和3，而代码1一般优于代码3，有时会被代码3超过；而在IE 6.0下，测试压力较大的时候（如测试10000次以上）代码2和3则有时候优于代码1，有时候就会远远落后代码1，而在测试压力较小（如5000次），则代码2&gt;代码3&gt;代码1。</p>
<h3>代码4：</h3>
<pre class="js" title="code">var i = 0;
var a;
while(i &lt; 100){
a = 0;
i++;
}</pre>
<h3>代码5：</h3>
<pre class="js" title="code">var a;
for(var i=0;i&lt;100;i++){
a = 0;
}</pre>
<p>上面两段代码在Firefox和IE下测试结果都是性能接近的。</p>
<h3>代码6：</h3>
<pre class="js" title="code">var a;
var i=0;
while(i&lt;100){
a=i;
i++;
}</pre>
<h3>代码7：</h3>
<pre class="js" title="code">var a;
var i=0;
while(i&lt;100){
a=i++;
}</pre>
<h3>代码8：</h3>
<pre class="js" title="code">var a;
for(var i=0;i&lt;100;i++){
a = i;
}</pre>
<h3>代码9：</h3>
<pre class="js" title="code">var a;
for(var i=0;i&lt;100;){
a = i++;
}</pre>
<p>这四段代码在Firefox下6和8的性能接近，7和9的性能接近，而6,</p>
<p>8 &lt; 7, 9；</p>
<p>最后我们来看一下空循环</p>
<h3>代码10：</h3>
<pre class="js" title="code">for(var i=0;i&lt;100;i++){   }</pre>
<h3>代码11：</h3>
<pre class="js" title="code">var i;
while(i&lt;100){
  i++;
}</pre>
<p>最后的测试出现了神奇的结果，Firefox下代码10所花的时间与代码11所花的大约是24:1。所以它不具备参考价值，于是我没有放在一开始给大家看。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.fangyuqiang.com/archives/417/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>[译]MYSQL中分层数据的管理</title>
		<link>http://www.fangyuqiang.com/archives/382</link>
		<comments>http://www.fangyuqiang.com/archives/382#comments</comments>
		<pubDate>Tue, 21 Jul 2009 15:46:36 +0000</pubDate>
		<dc:creator>fangyuqiang</dc:creator>
				<category><![CDATA[数据库]]></category>
		<category><![CDATA[数据结构]]></category>
		<category><![CDATA[算法]]></category>

		<guid isPermaLink="false">http://www.fangyuqiang.com/?p=382</guid>
		<description><![CDATA[在看cakephp的API的时候发现的一篇介绍多级树形结构数据的管理，本来想找时间翻译的，发现已经有了翻译版本的，感谢yimin这位翻译者，可惜找不到他的链接了。]]></description>
			<content:encoded><![CDATA[<p>在看cakephp的API的时候发现的一篇介绍多级树形结构数据的管理，本来想找时间翻译的，发现已经有了翻译版本的，感谢yimin这位翻译者，可惜找不到他的链接了。</p>
<h3>MYSQL中分层数据的管理</h3>
<p>By Mike Hillyer</p>
<h3>引言</h3>
<p>大多数用户都曾在数据库中处理过分层数据(hierarchical data)，认为分层数据的管理不是关系数据库的目的。之所以这么认为，是因为关系数据库中的表没有层次关系，只是简单的平面化的列表；而分层数据具有父－子关系，显然关系数据库中的表不能自然地表现出其分层的特性。</p>
<p>我们认为，分层数据是每项只有一个父项和零个或多个子项（根项除外，根项没有父项）的数据集合。分层数据存在于许多基于数据库的应用程序中，包括论坛和邮件列表中的分类、商业组织图表、内容管理系统的分类、产品分类。我们打算使用下面一个虚构的电子商店的</p>
<p>产品分类：</p>
<p>这些分类层次与上面提到的一些例子中的分类层次是相类似的。在本文中我们将从传统的邻接表(adjacency list)模型出发，阐述2种在MySQL中处理分层数据的模型。</p>
<h3>邻接表模型</h3>
<p>上述例子的分类数据将被存储在下面的数据表中（我给出了全部的数据表创建、数据插入的代码，你可以跟着做）：</p>
<pre class="sql" title="code">CREATE TABLE category(
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
parent INT DEFAULT NULL);
INSERT INTO category
VALUES(1,'ELECTRONICS',NULL),(2,'TELEVISIONS',1),(3,'TUBE',2),
(4,'LCD',2),(5,'PLASMA',2),(6,'PORTABLE ELECTRONICS',1),
(7,'MP3 PLAYERS',6),(8,'FLASH',7),
(9,'CD PLAYERS',6),(10,'2 WAY RADIOS',6);
SELECT * FROM category ORDER BY category_id;</pre>
<pre class="sql" title="code">+-------------+----------------------+--------+
| category_id | name                 | parent |
+-------------+----------------------+--------+
|           1 | ELECTRONICS          |   NULL |
|           2 | TELEVISIONS           |      1 |
|           3 | TUBE                     |      2 |
|           4 | LCD                       |      2 |
|           5 | PLASMA                 |      2 |
|           6 | PORTABLE ELECTRONICS |      1 |
|           7 | MP3 PLAYERS          |      6 |
|           8 | FLASH                |      7 |
|           9 | CD PLAYERS           |      6 |
|          10 | 2 WAY RADIOS         |      6 |
+-------------+----------------------+--------+
10 rows in set (0.00 sec)</pre>
<p>在邻接表模型中，数据表中的每项包含了指向其父项的指示器。在此例中，最上层项的父项为空值(NULL)。邻接表模型的优势在于它很简单，可以很容易地看出FLASH是MP3 PLAYERS的子项，哪个是portable electronics的子项，哪个是electronics的子项。虽然，在客户端编码中邻接表模型处理起来也相当的简单，但是如果是纯SQL编码的话，该模型会有很多问题。</p>
<h3>检索整树</h3>
<p>通常在处理分层数据时首要的任务是，以某种缩进形式来呈现一棵完整的树。为此，在纯SQL编码中通常的做法是使用自连接(self-join)：</p>
<pre class="sql" title="code">SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
WHERE t1.name = 'ELECTRONICS';

+-------------+----------------------+--------------+-------+
| lev1        | lev2                 | lev3         | lev4  |
+-------------+----------------------+--------------+-------+
| ELECTRONICS | TELEVISIONS          | TUBE         | NULL  |
| ELECTRONICS | TELEVISIONS          | LCD          | NULL  |
| ELECTRONICS | TELEVISIONS          | PLASMA       | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS  | FLASH |
| ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS   | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL  |
+-------------+----------------------+--------------+-------+
6 rows in set (0.00 sec)</pre>
<h3>检索所有叶子节点</h3>
<p>我们可以用左连接(LEFT JOIN)来检索出树中所有叶子节点(没有孩子节点的节点）：</p>
<pre class="sql" title="code">SELECT t1.name FROM
category AS t1 LEFT JOIN category as t2
ON t1.category_id = t2.parent
WHERE t2.category_id IS NULL;
+--------------+
| name         |
+--------------+
| TUBE         |
| LCD          |
| PLASMA       |
| FLASH        |
| CD PLAYERS   |
| 2 WAY RADIOS |
+--------------+</pre>
<h3>检索单一路径</h3>
<p>通过自连接，我们也可以检索出单一路径：</p>
<pre class="sql" title="code">SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
WHERE t1.name = 'ELECTRONICS' AND t4.name = 'FLASH';
+-------------+----------------------+-------------+-------+
| lev1        | lev2                 | lev3        | lev4  |
+-------------+----------------------+-------------+-------+
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH |
+-------------+----------------------+-------------+-------+
1 row in set (0.01 sec)</pre>
<p>这种方法的主要局限是你需要为每层数据添加一个自连接，随着层次的增加，自连接变得越来越复杂，检索的性能自然而然的也就下降了。</p>
<h3>邻接表模型的局限性</h3>
<p>用纯SQL编码实现邻接表模型有一定的难度。在我们检索某分类的路径之前，我们需要知道该分类所在的层次。另外，我们在删除节点的时候要特别小心，因为潜在的可能会孤立一棵子树（当删除portable electronics分类时，所有他的子分类都成了孤儿）。部分局限性可以通过使用客户端代码或者存储过程来解决，我们可以从树的底部开始向上迭代来获得一颗树或者单一路径，我们也可以在删除节点的时候使其子节点指向一个新的父节点，来防止孤立子树的产生。</p>
<h3>嵌套集合(Nested Set)模型</h3>
<p>我想在这篇文章中重点阐述一种不同的方法，俗称为嵌套集合模型。在嵌套集合模型中，我们将以一种新的方式来看待我们的分层数据，不再是线与点了，而是嵌套容器。我试着以嵌套容器的方式画出了electronics分类图：</p>
<p>从上图可以看出我们依旧保持了数据的层次，父分类包围了其子分类。在数据表中，我们通过使用表示节点的嵌套关系的左值(left value)和右值(right value)来表现嵌套集合模型中数据的分层特性：</p>
<pre class="sql" title="code">CREATE TABLE nested_category (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL
);
INSERT INTO nested_category
VALUES(1,'ELECTRONICS',1,20),(2,'TELEVISIONS',2,9),(3,'TUBE',3,4),
(4,'LCD',5,6),(5,'PLASMA',7,8),(6,'PORTABLE ELECTRONICS',10,19),
(7,'MP3 PLAYERS',11,14),(8,'FLASH',12,13),
(9,'CD PLAYERS',15,16),(10,'2 WAY RADIOS',17,18);
SELECT * FROM nested_category ORDER BY category_id;
+-------------+----------------------+-----+-----+
| category_id | name                 | lft | rgt |
+-------------+----------------------+-----+-----+
|           1 | ELECTRONICS          |   1 |  20 |
|           2 | TELEVISIONS          |   2 |   9 |
|           3 | TUBE                 |   3 |   4 |
|           4 | LCD                  |   5 |   6 |
|           5 | PLASMA               |   7 |   8 |
|           6 | PORTABLE ELECTRONICS |  10 |  19 |
|           7 | MP3 PLAYERS          |  11 |  14 |
|           8 | FLASH                |  12 |  13 |
|           9 | CD PLAYERS           |  15 |  16 |
|          10 | 2 WAY RADIOS         |  17 |  18 |
+-------------+----------------------+-----+-----+</pre>
<p>我们使用了lft和rgt来代替left和right，是因为在MySQL中left和right是保留字。http://dev.mysql.com/doc/mysql/en/reserved-words.html，有一份详细的MySQL保留字清单。那么，我们怎样决定左值和右值呢？我们从外层节点的最左侧开始，从左到右编号：</p>
<p>这样的编号方式也同样适用于典型的树状结构：</p>
<p>当我们为树状的结构编号时，我们从左到右，一次一层，为节点赋右值前先从左到右遍历其子节点给其子节点赋左右值。这种方法被称作改进的先序遍历算法。</p>
<h3>检索整树</h3>
<p>我们可以通过自连接把父节点连接到子节点上来检索整树，是因为子节点的lft值总是在其父节点的lft值和rgt值之间：</p>
<pre class="sql" title="code">SELECT node.name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND parent.name = 'ELECTRONICS'
ORDER BY node.lft;
+----------------------+
| name                 |
+----------------------+
| ELECTRONICS          |
| TELEVISIONS          |
| TUBE                 |
| LCD                  |
| PLASMA               |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS          |
| FLASH                |
| CD PLAYERS           |
| 2 WAY RADIOS         |
+----------------------+</pre>
<p>不像先前邻接表模型的例子，这个查询语句不管树的层次有多深都能很好的工作。在BETWEEN的子句中我们没有去关心node的rgt值，是因为使用node的rgt值得出的父节点总是和使用lft值得出的是相同的。</p>
<h3>检索所有叶子节点</h3>
<p>检索出所有的叶子节点，使用嵌套集合模型的方法比邻接表模型的LEFT JOIN方法简单多了。如果你仔细得看了nested_category表，你可能已经注意到叶子节点的左右值是连续的。要检索出叶子节点，我们只要查找满足rgt=lft+1的节点：</p>
<pre class="sql" title="code">SELECT name
FROM nested_category
WHERE rgt = lft + 1;
+--------------+
| name         |
+--------------+
| TUBE         |
| LCD          |
| PLASMA       |
| FLASH        |
| CD PLAYERS   |
| 2 WAY RADIOS |
+--------------+</pre>
<h3>检索单一路径</h3>
<p>在嵌套集合模型中，我们可以不用多个自连接就可以检索出单一路径：</p>
<pre class="sql" title="code">SELECT parent.name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'FLASH'
ORDER BY node.lft;
+----------------------+
| name                 |
+----------------------+
| ELECTRONICS          |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS          |
| FLASH                |
+----------------------+</pre>
<h3>检索节点的深度</h3>
<p>我们已经知道怎样去呈现一棵整树，但是为了更好的标识出节点在树中所处层次，我们怎样才能检索出节点在树中的深度呢？我们可以在先前的查询语句上增加COUNT函数和GROUP BY子句来实现：</p>
<pre class="sql" title="code">SELECT node.name, (COUNT(parent.name) 1)
AS depth
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| ELECTRONICS          |     0 |
| TELEVISIONS          |     1 |
| TUBE                 |     2 |
| LCD                  |     2 |
| PLASMA               |     2 |
| PORTABLE ELECTRONICS |     1 |
| MP3 PLAYERS          |     2 |
| FLASH                |     3 |
| CD PLAYERS           |     2 |
| 2 WAY RADIOS         |     2 |
+----------------------+-------+</pre>
<p>我们可以根据depth值来缩进分类名字，使用CONCAT和REPEAT字符串函数1：</p>
<pre class="sql" title="code">SELECT CONCAT( REPEAT(' ', COUNT(parent.name) 1),
node.name) AS name
FROM nested_category AS node,
nested_category AS paren
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
+-----------------------+</pre>
<p>当然，在客户端应用程序中你可能会用depth值来直接展示数据的层次。Web开发者会遍历该树，随着depth值的增加和减少来添加和标签。</p>
<h3>检索子树的深度</h3>
<p>当我们需要子树的深度信息时，我们不能限制自连接中的node或parent，因为这么做会打乱数据集的顺序。因此，我们添加了第三个自连接作为子查询，来得出子树新起点的深度值：</p>
<pre class="sql" title="code">SELECT node.name, (COUNT(parent.name) (
sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
nested_category AS parent,
nested_category AS sub_parent,
(
SELECT node.name, (COUNT(parent.name) 1)
AS depth
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'PORTABLE ELECTRONICS'
GROUP BY node.name
ORDER BY node.lft
)AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.name = sub_tree.name
GROUP BY node.name
ORDER BY node.lft;
+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| PORTABLE ELECTRONICS |     0 |
| MP3 PLAYERS          |     1 |
| FLASH                |     2 |
| CD PLAYERS           |     1 |
| 2 WAY RADIOS         |     1 |
+----------------------+-------+</pre>
<p>这个查询语句可以检索出任一节点子树的深度值，包括根节点。这里的深度值跟你指定的节点有关。</p>
<h3>检索节点的直接子节点</h3>
<p>可以想象一下，你在零售网站上呈现电子产品的分类。当用户点击分类后，你将要呈现该分类下的产品，同时也需列出该分类下的直接子分类，而不是该分类下的全部分类。为此，我们只呈现该节点及其直接子节点，不再呈现更深层次的节点。例如，当呈现PORTABLE ELECTRONICS分类时，我们同时只呈现MP3 PLAYERS、CD PLAYERS和2 WAY RADIOS分类，而不呈现FLASH分类。要实现它非常的简单，在先前的查询语句上添加HAVING子句：</p>
<pre class="sql" title="code">SELECT node.name, (COUNT(parent.name) (
sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
nested_category AS parent,
nested_category AS sub_parent,
(
SELECT node.name, (COUNT(parent.name) 1)
AS depth
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'PORTABLE ELECTRONICS'
GROUP BY node.name
ORDER BY node.lft
)AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.name = sub_tree.name
GROUP BY node.name
HAVING depth &lt;= 1
ORDER BY node.lft;
+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| PORTABLE ELECTRONICS |     0 |
| MP3 PLAYERS          |     1 |
| CD PLAYERS           |     1 |
| 2 WAY RADIOS         |     1 |
+----------------------+-------+</pre>
<p>如果你不希望呈现父节点，你可以更改HAVING depth &lt;= 1为HAVING depth = 1。</p>
<h3>嵌套集合模型中集合函数的应用</h3>
<p>让我们添加一个产品表，我们可以使用它来示例集合函数的应用：</p>
<pre class="sql" title="code">CREATE TABLE product(
product_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(40),
category_id INT NOT NULL
);
INSERT INTO product(name, category_id) VALUES('20" TV',3),('36" TV',3),
('SuperLCD
42"',4),('UltraPlasma
62"',5),('Value Plasma 38"',5),
('PowerMP3
5gb',7),('SuperPlayer
1gb',8),('Porta CD',9),('CD To go!',9),
('Family Talk 360',10);
SELECT * FROM product;
+------------+-------------------+-------------+
| product_id | name              | category_id |
+------------+-------------------+-------------+
|          1 | 20" TV            |           3 |
|          2 | 36" TV            |           3 |
|          3 | Super-LCD 42"     |           4 |
|          4 | Ultra-Plasma 62"  |           5 |
|          5 | Value Plasma 38"  |           5 |
|          6 | Power-MP3 128mb   |           7 |
|          7 | Super-Shuffle 1gb |           8 |
|          8 | Porta CD          |           9 |
|          9 | CD To go!         |           9 |
|         10 | Family Talk 360   |          10 |
+------------+-------------------+-------------+</pre>
<p>现在，让我们写一个查询语句，在检索分类树的同时，计算出各分类下的产品数量：</p>
<pre class="sql" title="code">SELECT parent.name, COUNT(product.name)
FROM nested_category AS node ,
nested_category AS parent,
product
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.category_id = product.category_id
GROUP BY parent.name
ORDER BY node.lft;
+----------------------+---------------------+
| name                 | COUNT(product.name) |
+----------------------+---------------------+
| ELECTRONICS          |                  10 |
| TELEVISIONS          |                   5 |
| TUBE                 |                   2 |
| LCD                  |                   1 |
| PLASMA               |                   2 |
| PORTABLE ELECTRONICS |                   5 |
| MP3 PLAYERS          |                   2 |
| FLASH                |                   1 |
| CD PLAYERS           |                   2 |
| 2 WAY RADIOS         |                   1 |
+----------------------+---------------------+</pre>
<p>这条查询语句在检索整树的查询语句上增加了COUNT和GROUP BY子句，同时在WHERE子句中引用了product表和一个自连接。</p>
<h3>新增节点</h3>
<p>到现在，我们已经知道了如何去查询我们的树，是时候去关注一下如何增加一个新节点来更新我们的树了。让我们再一次观察一下我们的嵌套集合图：</p>
<p>当我们想要在TELEVISIONS和PORTABLE ELECTRONICS节点之间新增一个节点，新节点的lft和rgt 的 值为10和11，所有该节点的右边节点的lft和rgt值都将加2，之后我们再添加新节点并赋相应的lft和rgt值。在MySQL 5中可以使用存储过程来完成，我假设当前大部分读者使用的是MySQL 4.1版本，因为这是最新的稳定版本。所以，我使用了锁表（LOCK TABLES）语句来隔离查询：</p>
<pre class="sql" title="code">LOCK TABLE nested_category WRITE;
SELECT @myRight := rgt FROM nested_category
WHERE name = 'TELEVISIONS';
UPDATE nested_category SET rgt = rgt + 2 WHERE rgt &gt; @myRight;
UPDATE nested_category SET lft = lft + 2 WHERE lft &gt; @myRight;
INSERT INTO nested_category(name, lft, rgt) VALUES('GAME CONSOLES', @myRight + 1,
@myRight + 2);
UNLOCK TABLES;</pre>
<p>我们可以检验一下新节点插入的正确性：</p>
<pre class="sql" title="code">SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) 1)
), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  GAME CONSOLES        |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
+-----------------------+</pre>
<p>如果我们想要在叶子节点下增加节点，我们得稍微修改一下查询语句。让我们在2 WAY RADIOS叶子节点下添加FRS节点吧：</p>
<pre class="sql" title="code">LOCK TABLE nested_category WRITE;
SELECT @myLeft := lft FROM nested_category
WHERE name = '2 WAY RADIOS';
UPDATE nested_category SET rgt = rgt + 2 WHERE rgt &gt; @myLeft;
UPDATE nested_category SET lft = lft + 2 WHERE lft &gt; @myLeft;
INSERT INTO nested_category(name, lft, rgt) VALUES('FRS', @myLeft + 1, @myLeft +
2);
UNLOCK TABLES;</pre>
<p>在这个例子中，我们扩大了新产生的父节点(2 WAY RADIOS节点）的右值及其所有它的右边节点的左右值，之后置新增节点于新父节点之下。正如你所看到的，我们新增的节点已经完全融入了嵌套集合中：</p>
<pre class="sql" title="code">SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) 1)
), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  GAME CONSOLES        |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+</pre>
<h3>删除节点</h3>
<p>最后还有个基础任务，删除节点。删除节点的处理过程跟节点在分层数据中所处的位置有关，删除一个叶子节点比删除一个子节点要简单得多，因为删除子节点的时候，我们需要去处理孤立节点。删除一个叶子节点的过程正好是新增一个叶子节点的逆过程，我们在删除节点的同时该节点右边所有节点的左右值和该父节点的右值都会减去该节点的宽度值2：</p>
<pre class="sql" title="code">LOCK TABLE nested_category WRITE;
SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt lft
+ 1
FROM nested_category
WHERE name = 'GAME CONSOLES';
DELETE FROM nested_category WHERE lft BETWEEN @myLeft AND @myRight;
UPDATE nested_category SET rgt = rgt @
myWidth WHERE rgt &gt; @myRight;
UPDATE nested_category SET lft = lft @
myWidth WHERE lft &gt; @myRight;
UNLOCK TABLES;</pre>
<p>我们再一次检验一下节点已经成功删除,而且没有打乱数据的层次：</p>
<pre class="sql" title="code">SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) 1)
), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+</pre>
<p>这个方法可以完美地删除节点及其子节点：</p>
<pre class="sql" title="code">LOCK TABLE nested_category WRITE;
SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt lft
+ 1
FROM nested_category</pre>
<p>2 [译注] 作者的本意是举删除叶子节点的情况，不过他给出的查询语句是通用型的，删除节点及其子节点，跟下例的查询语句是相同的。</p>
<pre class="sql" title="code">WHERE name = 'MP3 PLAYERS';
DELETE FROM nested_category WHERE lft BETWEEN @myLeft AND @myRight;
UPDATE nested_category SET rgt = rgt @
myWidth WHERE rgt &gt; @myRight;
UPDATE nested_category SET lft = lft @
myWidth WHERE lft &gt; @myRight;
UNLOCK TABLES;</pre>
<p>再次验证我们已经成功的删除了一棵子树：</p>
<pre class="sql" title="code">SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) 1)
), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+</pre>
<p>有时，我们只删除该节点，而不删除该节点的子节点。在一些情况下，你希望改变其名字为占位符，直到替代名字的出现，比如你开除了一个主管（需要更换主管）。在另外一些情况下，你希望子节点挂到该删除节点的父节点下：</p>
<pre class="sql" title="code">LOCK TABLE nested_category WRITE;
SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt lft
+ 1
FROM nested_category
WHERE name = 'PORTABLE ELECTRONICS';
DELETE FROM nested_category WHERE lft = @myLeft;
UPDATE nested_category SET rgt = rgt 1,
lft = lft 1
WHERE lft BETWEEN @myLeft
AND @myRight;
UPDATE nested_category SET rgt = rgt 2
WHERE rgt &gt; @myRight;
UPDATE nested_category SET lft = lft 2
WHERE lft &gt; @myRight;
UNLOCK TABLES;</pre>
<p>在这个例子中，我们对该节点所有右边节点的左右值都减去了2（因为不考虑其子节点，该节点的宽度为2），对该节点的子节点的左右值都减去了1（弥补由于失去父节点的左值造成的裂缝）。我们再一次确认，那些节点是否都晋升了：</p>
<pre class="sql" title="code">SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) 1)
), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+---------------+
| name          |
+---------------+
| ELECTRONICS   |
|  TELEVISIONS  |
|   TUBE        |
|   LCD         |
|   PLASMA      |
|  CD PLAYERS   |
|  2 WAY RADIOS |
|   FRS         |
+---------------+</pre>
<p>有时，当删除节点的时候，把该节点的一个子节点挂载到该节点的父节点下，而其他节点挂到该节点父节点的兄弟节点下，考虑到篇幅这种情况不在这里解说了。</p>
<h3>最后的思考</h3>
<p>我希望这篇文章对你有所帮助，SQL中的嵌套集合的观念大约有十年的历史了，在网上和一些书中都能找到许多相关信息。在我看来，讲述分层数据的管理最全面的，是来自一本名叫《Joe Celko&#8217;s Trees and Hierarchies in SQL for Smarties》3的书，此书的作者是在高</p>
<p>级SQL领域倍受尊敬的Joe Celko。Joe Celko被认为是嵌套集合模型的创造者，更是该领域内的多产作家。我把Celko的书当作无价之宝，并极力地推荐它。在这本书中涵盖了在此文中没有提及的一些高级话题，也提到了其他一些关于邻接表和嵌套集合模型下管理分层数据的方法。</p>
<p>在随后的参考书目章节中4，我列出了一些网络资源，也许对你研究分层数据的管理会有所帮助，其中包括一些PHP相关的资源（处理嵌套集合的PHP库）。如果你还在使用邻接表模型，你该去试试嵌套集合模型了，在Storing Hierarchical Data in a Databas e5 文中下方列出的一些资源链接中能找到一些样例代码，可以去试验一下。</p>
<h3>关于作者/译者</h3>
<p>Mike Hillyer，本文的作者，MySQL Ab的技术作家，生活在加拿大的阿尔伯达省6 。</p>
<p>Yimin，本文的译者，就读于浙江理工大学计算机系。我的Blog：http://liyimin.net/blog</p>
<p>6 [译注]加拿大西部一省份，位于不列颠哥伦比亚省和萨斯喀彻温省之间。于1905年加入联邦。在六十年代初发现石油和天然气以前，小麦种植和养牛业为该省经济的主要支柱。埃德蒙顿为该省首府及最大的城市。人口2,237,724</p>
]]></content:encoded>
			<wfw:commentRss>http://www.fangyuqiang.com/archives/382/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
