《编程之美》1.3一摞烙饼的排序

《编程之美》1.3一摞烙饼的排序

http://blog.csdn.net/kabini/article/details/2276723。代码摘抄自《编程之美》相应章节并做了一些修改。
      文中最关键的代码就一处,就是调用递归的地方。
//递归进行翻转
for (i = 1; i < m_nCakeCnt; i++) {
        Revert(0, i);
	m_ReverseCakeArraySwap[step] = i;
	Search(step + 1);
	Revert(0, i);
	}
      我们先来理解这个Search()递归函数干了什么,参数只有一个,即第几步,或者第几次。我们常用的递归中,一般都是一条线向下纵深递归,直到找到答案,再层层返回。但是这里的递归是树形的,每向下纵深一层,都会出现多个向下分支(就像树上每个节点都有多个子节点),也就是上面for循环的作用,每一次for循环就是一个子节点。
      书中的思路是给我一个数组,把这个数组当成树的根节点(每个节点代表一个数组,一种顺序),找到这个数组翻转一次所有可能的结果,也就是反转前两个,前三个,前N个,一直到反转整个数组。把每一种结果都当做这个跟节点的子节点。然后再以同样的方法找到每个子节点的子节点。这样就形成一个无线向下拓展的树,每一条从根节点到末节点的路径,就是一个翻转顺序,翻转方案,根节点(最原始数组)调用search时step是0,第一层子节点step是1,第二层子节点是2,以此类推。下图很形象的说明了书中的思路。

      当然我们是不用一直这样向下拓展的,每向下拓展一层,说明就要翻转一次。我们知道一摞烙饼最多要翻转的次数(书中说是2(n-1),但是我认为应该是2(n-2)+1)。也就是最多拓展到2(n-2)+1层即可。我们还知道最少需要翻转的次数,书中的算法我觉得也有问题,如果烙饼的次序就是从上面往下数是从大到小的顺序,按他的计算方法,最小次数就是0.但是其实应该是1,需要整个翻转一次。也就是他只判断了两两关系,而没有判断顺序。当我们递归到第N层的时候,只要判断已经翻转的次数(step)加上当前数组(翻转step次后的顺序)还需要的最少的次数如果大于翻转原始数组的上界次数,那么我们就能知道,这条纵深,不是最佳翻转顺序。也就是书中第一种return的情况。
      当突然发现,某个节点的数组顺序已经翻转好了。那么这个节点的深度就是翻转的次数。这不一定是最优的,但是却有了比最大翻转次数小的翻转方案,所以后面可以以当前这个翻转次数为最大翻转次数,继续找比这更优的次数。就是第二个return。
      回过头来,我们再看看那个最不好理解,递归调用的代码。
//递归进行翻转
for (i = 1; i < m_nCakeCnt; i++) {
	Revert(0, i);
	m_ReverseCakeArraySwap[step] = i;
	Search(step + 1);
	Revert(0, i);
	}
      上面已经说过,for循环是遍历所有的翻转方法,看看两个Reverse(o,i),其实就是翻转了,然后再翻转回来。因为在书中的递归中,翻转的数组始终只有一个m_ReverseCakeArray,如果每次递归都创建新的数组可能就好理解一点了。因为书中始终翻转一个数组,当退出一次递归的时候,必须将数组还原到之前的状态(顺序),继续下一次递归。。
      m_ReverseCakeArraySwap[step] = i; 记录翻转步骤。
      对书中代码中的一些修改或改进:
      [1]变量m_arrSwap应该是m_SwapArray,因为m_arrSwap从未被定义过;
      [2]upBound()方法中应改为return (nCakeCnt-1)*2,当然这个上界不改也行,只是我想这里应该和前面说的相对应。也可以优化为return (nCakeCnt - 2) * 2 + 1,当然这也不是最优解。。。
      [3]Search()方法的剪枝部分,if( step+nElimate > m_nMaxSwap)有误,因为nElimate可能为0,所以当step等于m_nMaxSwap时候 会造成下面的m_reverseCakeArraySwap[step] = i这个语句的数组越界。所以应该改为:
if( step + nEstimate >= m_nMaxSwap)。
      [4]判断下界时,如果最大的烙饼不在最后一个位置,则要多翻转一次,因而在LowerBound函数return ret; 前插入行:
 if (pCakeArray[nCakeCnt-1] != nCakeCnt-1)
      ret++;
下面是修改后的代码
#include<iostream>
#include<assert.h>
using namespace std;

/****************/
//
//烙饼排序的实现
//
/****************/
class CPrefixSorting {
private:
	int* m_CakeArray;     //烙饼信息数组
	int m_nCakeCnt;       //烙饼个数
	int m_nMaxSwap;       //最多交换次数。根据前面的推断,这里最多为(m_nCakeCnt-2)*2+1
	int* m_SwapArray;     //交换结果数组
	int* m_ReverseCakeArray;      //当前翻转烙饼信息数组
	int* m_ReverseCakeArraySwap;  //当前翻转烙饼交换结果数组
	int m_nSearch;                //当前搜索次数信息

public:
	CPrefixSorting(){   //构造函数
		m_nCakeCnt = 0;
		m_nMaxSwap = 0;
	}
	~CPrefixSorting() {  //析构函数
		if (m_CakeArray != NULL) {
			delete  m_CakeArray;
		}
		if (m_SwapArray != NULL) {
			delete m_SwapArray;
		}
		if (m_ReverseCakeArray != NULL) {
			delete m_ReverseCakeArray;
		}
		if (m_ReverseCakeArraySwap != NULL) {
			delete m_ReverseCakeArraySwap;
		}
	}
	
	//
	//计算烙饼翻转信息
	//@param
	//pCakeArray  存储烙饼索引数组
	//nCakeCnt     烙饼个数
	//
	void Run(int* pCakeArray, int nCakeCnt) {
		Init(pCakeArray, nCakeCnt);
		m_nSearch = 0;
		Search(0);
	}
	
	//
	//输出烙饼翻转次数
	//
	void Output() {
		for (int i = 0; i < m_nMaxSwap; i++) {
			cout << m_SwapArray[i] << " ";
		}
		cout << endl << " |Search Times| : " << m_nSearch << endl;
		cout << "Total Swap Times = " << m_nMaxSwap << endl;
	}

private:
	//
	//初始化数组信息
	//@param
	//pCakeArray  存储烙饼索引数组
	//nCakeCnt
	//
	void Init(int* pCakeArray, int nCakeCnt) {
		assert(pCakeArray != NULL);
		assert(nCakeCnt > 0);
		m_nCakeCnt = nCakeCnt;
		
		//初始化烙饼数组
		m_CakeArray = new int[m_nCakeCnt];
		assert(m_CakeArray != NULL);
		for (int i = 0; i < m_nCakeCnt; i++) {
			m_CakeArray[i] = pCakeArray[i];
		}

		//设置最多交换次数信息
		m_nMaxSwap = UpBound(m_nCakeCnt);

		//初始化交换结果数组
		m_SwapArray = new int[m_nMaxSwap + 1];
		assert(m_SwapArray!=NULL);

		//初始化中间交换结果信息
		m_ReverseCakeArray = new int[m_nCakeCnt];
		for (int i = 0; i < m_nCakeCnt; i++) {
			m_ReverseCakeArray[i] = m_CakeArray[i];
		}
		m_ReverseCakeArraySwap = new int[m_nMaxSwap];
	}

	//
	//寻找当前翻转的上界
	//
	int UpBound(int nCakeCnt) {
		return (nCakeCnt - 2) * 2 + 1;//原先return (nCakeCnt-1)*2也可以,
									  //代码这么写也没问题,只不过不是最优解而已
	}
	
	//
	//寻找当前翻转的下界
	//
	int LowerBound(int* pCakeArray, int nCakeCnt) {
		int t, ret = 0;

		//根据当前数组排序信息情况判断至少需要交换多少次
		for (int i = 1; i < nCakeCnt; i++) {
			//判断位置相邻的两个烙饼,是否为尺寸排序上相邻的
			//此处应该考虑顺序问题,若烙饼的次序从上往下数是从大到小的,即t==-1
			//翻转次数应该是1而非0,即要整个翻转一次。
			t = pCakeArray[i] - pCakeArray[i - 1];
			if ((t == 1) || (t == -1)) {
			}
			else {
				ret++;
			}
		}
		//判断下界时,如果最大的烙饼不在最后一个位置,则要多翻转一次(包含了t==-1的情况)
		//能有效减少无效搜索次数,虽然还是会包含无效搜索。。
		if (pCakeArray[nCakeCnt - 1] != nCakeCnt - 1)
			ret++;
		return ret;
	}

	//排序的主函数
	void Search(int step) {
		int i, nEstimate;
		m_nSearch++;

		//估算这次搜索所需要的最小交换次数nEstimate
		nEstimate = LowerBound(m_ReverseCakeArray, m_nCakeCnt);
		//根节点(最原始数组)调用search时step是0,第一层子节点step是1,
		//第二层子节点step是2,以此类推可知step是从0开始计数的。因为nElimate可能为0,
		//所以当step等于m_nMaxSwap时候,会造成下面的m_reverseCakeArraySwap[step]=i;
		//的数组越界。所以判断条件应改为>=
		if (step + nEstimate >= m_nMaxSwap)
			return;

		//如果已经排序好,即翻转完成,输出结果
		if (IsSorted(m_ReverseCakeArray, m_nCakeCnt)) {
			if (step < m_nMaxSwap) {
				m_nMaxSwap = step;
				for (i = 0; i < m_nMaxSwap; i++)
					m_SwapArray[i] = m_ReverseCakeArraySwap[i];
			}
			return;
		}

		//递归进行翻转
		for (i = 1; i < m_nCakeCnt; i++) {
			Revert(0, i);
			m_ReverseCakeArraySwap[step] = i;
			Search(step + 1);
			Revert(0, i);
		}
	}

	//
	//true:已经排好序
	//false:未排序
	//
	bool IsSorted(int* pCakeArray, int nCakeCnt) { //若数组内容从小到大有序,返回true
		for (int i = 1; i < nCakeCnt; i++) {
			if (pCakeArray[i - 1] > pCakeArray[i]) {
				return false;
			}
		}
		return true;
	}

	//
	//翻转烙饼信息
	//
	void Revert(int nBegin, int nEnd) { //把数组中给定两个系数之间的内容反序之~
		assert(nEnd > nBegin);
		int i, j, t;

		//翻转烙饼信息
		for (i = nBegin, j = nEnd; i < j; i++, j--) {
			t = m_ReverseCakeArray[i];
			m_ReverseCakeArray[i] = m_ReverseCakeArray[j];
			m_ReverseCakeArray[j] = t;
		}
	}
};

//主函数,供测试
int main() {
	int Cake[10] = {3,2,1,6,5,4,9,8,7,0};
	CPrefixSorting TestA;
	TestA.Run(Cake, 10);
	TestA.Output();
	return 0;
}
      运行结果如下图所示:

      感想:没有大的改变,理解原先代码已经不容易了,在成为菜鸡的路上越走越远了。。。。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值