标签归档:PHP算法

使用PHP实现堆排序

使用PHP实现堆排序
堆积排序(Heapsort)是指利用堆积树(堆)这种资料结构所设计的一种排序算法,可以利用数组的特点快速定位指定索引的元素。

“堆”定义
  n个关键字序列Kl,K2,…,Kn称为(Heap),当且仅当该序列满足如下性质(简称为堆性质):
  (1) ki≤K2i且ki≤K2i+1 或(2)Ki≥K2i且ki≥K2i+1(1≤i≤ n) //ki相当于二叉树的非叶结点,K2i则是左孩子,k2i+1是右孩子
  若将此序列所存储的向量R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:
  树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。

“筛选法”调整堆
  R[low]的左、右子树(若存在)均已是堆,这两棵子树的根R[2low]和R[2low+1]分别是各自子树中关键字最大的结点。若R[low].key不小于这两个孩子结点的关键字,则R[low]未违反堆[性质,以R[low]为根的树已是堆,无须调整;否则必须将R[low]和它的两个孩子结点中关键字较大者进行交换,即R[low]与R[large](R[large].key=max(R[2low].key,R[2low+1].key))交换。交换后又可能使结点R[large]违反堆性质,同样由于该结点的两棵子树(若存在)仍然是堆,故可重复上述的调整过程,对以R[large]为根的树进行调整。此过程直至当前被调整的结点已满足性质,或者该结点已是叶子为止。上述过程就象过筛子一样,把较小的关键字逐层筛下去,而将较大的关键字逐层选上来。因此,有人将此方法称为”筛选法”。
以上来自百度百科


以下为PHP的堆排序实现。
siftup函数是在x[1..n-1]为堆,在x[n]中放置一个任意的元素时,重新获得堆的操作。
它尽可能的将新元素向上筛选,向上筛选是通过交换该结点与其父结点来实现的。

siftdown函数是在x[1..n]是一个堆,在x[1]中放置一个任意的元素时,重新获得堆的操作。
它是一个向下筛选的过程,将顺序不对的元素和比它小的子结点交换。

通过siftup函数,加入n个元素,构造堆
通过siftdown函数,每次取堆顶端的数,然后重新构造堆,如此迭代,直到所有的数据都取出,因为堆顶一定是这个堆中最小的值,所以最后一定可以得到一个有序的序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php
/**
 * 堆排序
 * @author phppan.p#gmail.com  http://www.phppan.com
 * 哥学社成员(http://www.blog-brother.com/)
 * @package test
 */
 
/**
 * 向上筛选元素
 * 将元素$n添加到堆中,调整构建新的堆
 */
function siftup(&$seq, $n) {
	$i = $n;
 
	while($i > 1) {
		$p = floor($i / 2);
		if($seq[$p] <= $seq[$i]) {
			break;
		}
		list($seq[$p], $seq[$i]) = array($seq[$i], $seq[$p]);
 
		$i = $p;
	}
}
 
/**
 * 向下筛选元素
 */
function siftdown(&$seq, $n) {
	$i = 1;
 
	while(1) {
		$c = $i * 2;
 
		if($c > $n) {
			break;
		}
 
		/* $c 为左结点 $c + 1 为右结点*/
		if($c + 1 <= $n) {
			if($seq[$c + 1] < $seq[$c]) {
				$c++;
			}
		}
 
		if($seq[$i] <= $seq[$c]) {
			break;
		}
 
		/* 将$seq[$i]和它的两个孩子结点中关键字较大者进行交换 */
		list($seq[$c], $seq[$i]) = array($seq[$i], $seq[$c]);
 
		$i = $c;
	}
}
 
 
 
/**
 * 堆排序
 * @param	array	$seq	待排序的序列
 */
function heapSort(&$seq) {
	$n = count($seq);
 
	for($i = 2; $i <= $n; $i++) {
		siftup($seq, $i);
	}
 
	for($i = $n; $i >= 2; $i--) {
		list($seq[1], $seq[$i]) = array($seq[$i], $seq[1]);
		siftdown($seq, $i - 1);
	}
}
 
/* 测试 */
$seq = array(1 => 9, 7, 2, 3, 1, 6);
heapSort($seq);
print_r($seq);
 
die();

heapSort使用了n-1次siftup和n-1次siftdown操作,其时间复杂度为O(nlogn)

算法设计技术:一维模式识别

算法设计技术:一维模式识别
最近重温经典《编程珠玑》,在第8章算法设计技术中一维模式识别实例,书中举出了5种不同的解法,解法不断优化,不断的变得高效,不断得变得更优雅,看完感触良深。已经三年没有和算法有过直接沟通了,也淡忘了,从记忆中唤起,再次遇到,发现有些思想已经深入到骨子里了。
回正题。
【问题描述】
输入n个数的序列,输出这n个数的任意连续子序列的最大和。在这里我们假设序列的数都为整数(包括正和负)
如序列: 31 -41 59 26 -53 58 97 -93 -23 84
从[2..6]的总和187为最大

【解法一:穷举法】
最简单的方法,穷举法:即对所有满足0 <= i <= j < n 的(i, j)整数对进行迭代。对于每个整数对,程序都要计算x[i..j]的总和,并检验该总和是否大于迄今为止的最大总和。 其PHP实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
/**
 * 一维模式识别算法一,穷举法
 * @author phppan.p#gmail.com  http://www.phppan.com
 * 哥学社成员(http://www.blog-brother.com/)
 * @package test
 */
 
function maxsofar($seq) {
	$maxsofar = 0;
	$len = count($seq);
 
	for ($i = 0; $i < $len; $i++) {
		for ($j = $i; $j < $len; $j++) {
			$sum = 0;
			for ($k = $i; $k <= $j; $k++) {
				$sum += $seq[$k];
			}
 
			if($sum > $maxsofar) {
				$maxsofar = $sum;
			}
		}
	}
 
	return $maxsofar;
}
 
$seq = array(31, -41, 59, 26, -53, 58, 97, -93, -23, 84);
echo maxsofar($seq);
die();

算法一优点是非常清晰明了,一眼就看出其思路,并且实现简单。
但是其时间复杂度为O(n的立方),如果数据量稍微大一点,整个程序的效率将会巨慢。

【解法二:】
x[i..j]的总和与前面已计算的总和x[i..j-1]密切相关,即x[i..j] = x[i..j-1] + x[j]
于是我们有了解法二。
如下所示PHP代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
/**
 * 一维模式识别算法二,穷举法的优化算法,将时间复杂度变为n的平方
 * @author phppan.p#gmail.com  http://www.phppan.com
 * 哥学社成员(http://www.blog-brother.com/)
 * @package test
 */
 
function maxsofar($seq) {
	$maxsofar = 0;
	$len = count($seq);
 
	for ($i = 0; $i < $len; $i++) {
		$sum = 0;
		for ($j = $i; $j < $len; $j++) {
			$sum += $seq[$j];
 
			if($sum > $maxsofar) {
				$maxsofar = $sum;
			}
		}
	}
 
	return $maxsofar;
}
 
$seq = array(31, -41, 59, 26, -53, 58, 97, -93, -23, 84);
echo maxsofar($seq);
die();

【解法三:预处理】
同样是根据x[i..j]的总和与前面已计算的总和x[i..j-1]密切相关,即x[i..j] = x[i..j-1] + x[j]
不过我们使用另外一种表示:预处理数据,计算cumarr[i] = x[0..i],则x[i..j] = cumarr[j] – cumarr[i - 1]
然后再如解法二一样遍历比较,算出最大的值。
如下所示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
/**
 * 一维模式识别算法三,穷举法的优化算法,将时间复杂度变为n的平方
 * @author phppan.p#gmail.com  http://www.phppan.com
 * 哥学社成员(http://www.blog-brother.com/)
 * @package test
 */
 
function maxsofar($seq) {
	$maxsofar = 0;
	$len = count($seq);
 
	/* 预处理数据 */
	$cumarr[-1] = 0;
	for ($i = 0; $i < $len; $i++) {
		$cumarr[$i] = $cumarr[$i - 1] + $seq[$i];
	}
 
	for ($i = 0; $i < $len; $i++) {
		for ($j = $i; $j < $len; $j++) {
			$sum = $cumarr[$j] - $cumarr[$i - 1];
 
			if($sum > $maxsofar) {
				$maxsofar = $sum;
			}
		}
	}
 
	return $maxsofar;
}
 
$seq = array(31, -41, 59, 26, -53, 58, 97, -93, -23, 84);
echo maxsofar($seq);
die();

【解法四:分治算法】
分治原理:要解决规模为n的问题,可递归地解决两个规模近似为n/2的子问题,然后对它们的答案进行合并以得到整个问题的答案。
首先创建两个子序列a和b,然后递归找出a,b中元素总和最大的子向量,分别称为ma和mb,也许我们可能找到最后的解了,可是也有可能答案所在的子序列一部分在ma,一部分在mb,对于这种跨越边界的序列我们将其称之为mc。
我们的分治算法将递归地计算ma和mb,并通过其它方法计算mc,然后返回3个总各和中的最大者。
其PHP实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
/**
 * 一维模式识别算法四,分治算法
 * @author phppan.p#gmail.com  http://www.phppan.com
 * 哥学社成员(http://www.blog-brother.com/)
 * @package test
 */
 
function maxsofar($seq, $left, $right) {
	if($left > $right) {
		return 0;	//	已没有元素 递归返回
	}
 
	if($left == $right) {
		return max(0, $seq[$left]);	//	只有一个元素,返回这个元素与0之间的较大值
	}
 
	$middle = floor(($left + $right) / 2);
 
	/* 左边从中间开始的最大子序列和 */
	$lmax = $sum = 0;
	for($i = $middle; $i >= $left; $i--) {
		$sum += $seq[$i];
		$lmax = max($lmax, $sum);
	}
 
	/* 右边从中间开始的最大子序列和 */
	$rmax = $sum = 0;
	for($i = $middle + 1; $i <= $right; $i++) {
		$sum += $seq[$i];
		$rmax = max($rmax, $sum);
	}
 
	return max($lmax + $rmax, maxsofar($seq, $left, $middle), maxsofar($seq, $middle + 1, $right));
}
 
$seq = array(31, -41, 59, 26, -53, 58, 97, -93, -23, 84);
echo maxsofar($seq, 0, count($seq) - 1);
die();

对于mc的计算,通过观察发现,mc在a中包含右边界的最大子序列,而mc在了中包含左边界的最大子序列
此解法的时间复杂度为O(nlogn)

【解法五:扫描算法】
最大总和的初始值设为0,假设我们已解决了x[0..i-1]的问题,那么最大总各子序列要么在前i-1个元素中,要么其结束位置为i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
/**
 * 一维模式识别算法五,扫描算法
 * @author phppan.p#gmail.com  http://www.phppan.com
 * 哥学社成员(http://www.blog-brother.com/)
 * @package test
 */
 
function maxsofar($seq) {
	$maxsofar = 0;
	$maxendinghere = 0;
	$len = count($seq);
 
	for($i = 0; $i < $len; $i++) {
		$maxendinghere = max($maxendinghere + $seq[$i], 0);
		$maxsofar = max($maxsofar, $maxendinghere);
	}
 
	return $maxsofar;
}
 
$seq = array(31, -41, 59, 26, -53, 58, 97, -93, -23, 84);
echo maxsofar($seq);
die();

理解这个程序的关键在于变量$maxendinghere。
在循环中的第一个赋值语句之前,$maxendinghere是结束位置为i-1的最大子序列的和;赋值语句将其改为结束位置为i的最大子序列的和。
其时间复杂度为O(n),对于这种时间复杂度为O(n)的算法,一般称其为线性算法。

【后记】
从立方算法到线性算法,这是一个质的飞跃。
虽然现在的工作中可能用不到算法,但是在写程序的过程中能够不自觉的优化自己的代码,这也是一种好的习惯。
《编程珠玑》值得多看几次。