《编程之美》笔记-烙饼排序的思考

最近学习《 编程之美》第1.3节,最初自己看完题目在 Linux下使用 C语言进行了实现。其中题目要求输出最优化的方案(题目如下图),自己并没有想到很好的最优方案,因此只是按照自己的理解先进行了实现。

实现方法如下:
 
首先是建模,即将实际问题转化为程序实现方法。分析了翻饼过程类似于排序,只不过是特殊的排序,每次排序只能从开头元素到指定位置之间的元素进行逆序,因此 设定数组的开头代表一摞烙饼的最上端,每次对数组排序需从数组第一个元素开始。受排序冒泡实现启发,每次排序将最大的烙饼排到最底下,当最大的排在最底下(数组最末尾)时,可以对除最大之外的烙饼进行翻转,因此使用 递归实现。
 
下面是程序是实现(未看解答之前):
# include <stdio.h > 

void cakeSort( int cake[], int size); //排序函数,并输出排序方法
void reverse( int array[], int size); //数组逆序
int biggestFind( int array[], int size); //查找数组最大元素并输出位置

int step = 0;

int main( void)
{
     int cake[] = { 2, 4, 1, 5, 3};
     int size = sizeof(cake) / sizeof( int);
    cakeSort(cake, size);
    printf( "共需要%d步翻转\n", step);
     int i;
     for(i = 0; i < size; i ++) {
        printf( "%d", cake[i]);
    }
    printf( "\n");
     return 0;
}

void cakeSort( int cake[], int size)
{
     int maxLocation;
     if(size > 1) {
        maxLocation = biggestFind(cake, size); //获取最大值位置
         if(maxLocation == 0) { //最大值在开头,则reverse整个数组
            reverse(cake, size);
            step ++;
            printf( "第%d步:翻转上面的%d个饼\n", step, size);
            cakeSort(cake, size - 1);
        } else if(maxLocation == size - 1) { //最大值在末尾,则对前面的数字进行排序
            cakeSort(cake, size - 1);
        } else { //最大值在中间某位置,则将开始到最大值位置处的子数组进行reverse,使最大值放在开头处
            reverse(cake, maxLocation + 1);
            step ++;
            printf( "第%d步:翻转上面的%d个饼\n", step, maxLocation + 1);
            reverse(cake, size);
            step ++;
            printf( "第%d步:翻转上面的%d个饼\n", step, size);
            cakeSort(cake, size - 1);
        }
    }
}

//功能:返回数组中最大值所在的位置 
# include <stdio.h >
int biggestFind( int array[], int size)
{
     int location, max; //存储最大值所在位置和最大值
     int i;
     if(size < = 0)
    {
        printf( "Null array!\n");
         return - 1;
    }
    max = array[ 0];
     for(i = 0, location = 0; i < size; i ++)
    {
         if(array[i] > max)
        {
            max = array[i];
            location = i;
        }
    }
     return location;
}

//将数组逆置 
void reverse( int array[], int size)
{
     int i, temp;
     for(i = 0; i < size / 2; i ++) { 
        temp = array[size - 1 - i];
        array[size - 1 - i] = array[i];
        array[i] = temp;
    }
}
 
在Linux下使用gcc编译通过,测试了几个测试用例都没没有发现错误,至少实现了自己最初的设想。然后怀着些许满意的心情看了书中的解答,发现自己想的太简单了。然后又使用了一个测试用例:数组{2, 4, 1, 5, 3},发现确实是有问题的,如果使用自己的方法需要翻转7次才能达到结果,而如果先将2和4翻转一下,再按照上面的方法却只需要5次就可以。而且看完书后的感觉是自己根本就没按照题目要求来解答, 题目要求的是输出最优方案,也就程序需要解决的问题是查找最优的方案,而不是输出排序后的结果以及排序方案。
 
看完书中的解答,仍然不是太明白具体的解法,分析书中的程序,发现该实现使用了穷举法,穷举了所有的翻转方法,然后输出最优的方案,其中每次翻转仍然采用的递归方案。书中的程序存在一些小的问题,下面是我修正后侧程序实现,只修改了一点点。
C++程序实现:修改之处:1.将声明和实现分开;2.修正了程序中的一个印刷bug;3.修改了程序输出
CPrefixSorting.h
# ifndef CPREFIXSORTING_H 
# define CPREFIXSORTING_H

# include <assert.h >
# include <stdio.h >

class CPrefixSorting
{
     public :
        CPrefixSorting();
         ~CPrefixSorting();

         void Run( int * pCakeArray, int nCakeCnt); //计算最优翻转方法
         void Output(); //输出最优翻转实现
     protected :
     private :
         void Init( int * pCakeArray, int nCakeCnt); //初始化成员变量,实现了平常我们在构造函数中实现的功能
         int UpBound( int nCakeCnt); //计算上界
         int LowerBound( int * pCakeArray, int nCakeCnt); //计算下界
         void Search( int step); //排序的主函数,递归实现,查找出最优翻转方法
         bool IsSorted( int * pCakeArray, int nCakeCnt); //判断是否排序完成
         void Revert( int nBegin, int nEnd); //逆序一个数组
     private :
         int * m_CakeArray; //给每一个大小不一的烙饼设定一个数值,数值的大小代表烙饼的大小,相邻大小烙饼数值相差1,该数组存放烙饼的数值
         int m_nCakeCnt; //烙饼个数
         int m_nMaxSwap; //最大翻转次数,最初设为上界,即烙饼个数的2倍
         int * m_SwapArray; //存放翻转结果,数组的每一个数值代表翻转的上面的多少个烙饼

         int * m_ReverseCakeArray; //表示当前烙饼信息数组
         int * m_ReverseCakeArraySwap; //当前翻转烙饼交换信息数组
         int m_nSearch; // 表示进行了收索的次数
};
# endif // CPREFIXSORTING_H
 
CPrefixSorting.cpp  其中最重要的实现就是函数Search的实现
# include "/home/linux/BeautyOfProgramming/C1_3CakeSort_C++Version/CakeSort/include/CPrefixSorting.h" 
CPrefixSorting : :CPrefixSorting()
{
    m_nCakeCnt = 0;
    m_nMaxSwap = 0;
}
CPrefixSorting : : ~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;
    }
}
void CPrefixSorting : :Run( int * pCakeArray, int nCakeCnt)
{
    Init(pCakeArray, nCakeCnt);

    m_nSearch = 0;
    Search( 0);
}
void CPrefixSorting : :Output()
{
     for( int i = 0; i < m_nMaxSwap; i ++)
    {
        printf( "%d step: revert %d Cakes above\n", i, m_SwapArray[i]);
    }

    printf( "\n |Search Times| : %d\n", m_nSearch);
    printf( "Total Swap times = %d\n", m_nMaxSwap);
}
void CPrefixSorting : :Init( int * pCakeArray, int nCakeCnt)
{
    assert(pCakeArray != NULL);
    assert(nCakeCnt > 0);

    m_nCakeCnt = nCakeCnt;

     //init cake array
    m_CakeArray = new int[m_nCakeCnt];
    assert(m_CakeArray != NULL);
     for( int i = 0; i < m_nCakeCnt; i ++) {
        m_CakeArray[i] = pCakeArray[i];
    }

     //set max swap info
    m_nMaxSwap = UpBound(m_nCakeCnt);

     //init swap result array
    m_SwapArray = new int[m_nMaxSwap + 1];
    assert(m_SwapArray != NULL);

     //init swap result info
    m_ReverseCakeArray = new int[m_nCakeCnt];
    assert(m_ReverseCakeArray);
     for( int i = 0; i < m_nCakeCnt; i ++) {
        m_ReverseCakeArray[i] = m_CakeArray[i];
    }
    m_ReverseCakeArraySwap = new int[m_nMaxSwap];
    assert(m_ReverseCakeArraySwap);
}

//find upbound
int CPrefixSorting : :UpBound( int nCakeCnt)
{
     return nCakeCnt * 2;
}

//find downbound
int CPrefixSorting : :LowerBound( int * pCakeArray, int nCakeCnt)
{
     int t, ret = 0;

     //get the min number of swap, according the current sort info of array
     for( int i = 1; i < nCakeCnt; i ++)
    {
         //判断位置相邻的两个酪饼,是否为尺寸排序上相邻的
        t = pCakeArray[i] - pCakeArray[i - 1];
         if((t == 1) || (t == - 1))
        {
        }
         else
        {
            ret ++;
        }
    }
     return ret;
}
void CPrefixSorting : :Search( int step)
{
     int i, nEstimate;

    m_nSearch ++;

     //estimate the min search of swap
    nEstimate = LowerBound(m_ReverseCakeArray, m_nCakeCnt);
     if(step + nEstimate > m_nMaxSwap)
    {
         return;
    }
     //if sorting finished, output
     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 + 1;
        Search(step + 1);
        Revert( 0, i); //将翻转过的数组重置
    }
}
bool CPrefixSorting : :IsSorted( int * pCakeArray, int nCakeCnt)
{
     for( int i = 1; i < nCakeCnt; i ++)
    {
         if(pCakeArray[i - 1] > pCakeArray[i])
        {
             return false;
        }
    }
     return true;
}
void CPrefixSorting : :Revert( int nBegin, int nEnd)
{
    assert(nEnd > nBegin);
     int i, j, t;

     //revert info
     for(i = nBegin, j = nEnd; i < j; i ++, j --)
    {
        t = m_ReverseCakeArray[i];
        m_ReverseCakeArray[i] = m_ReverseCakeArray[j];
        m_ReverseCakeArray[j] = t;
    }
}
 
对上面的实现在linux下使用Code:blocks编译通过,并能输出正确结果。如下图所示:图中显示为使用测试用例:数组{2, 4, 1, 5, 3}的结果。

几点 总结
1. 学习上面C++实现中函数Search的实现, 穷举法与递归法组合实现,递归在穷举实现的循环中!!!
2. 学习这种迭代式的 思考方法,一开始的方案可能并不最优,但是不断的查找问题并解决。
3. 上面的方案仍然存在可优化的地方。虽然上面的方法肯定可以找到最优方案,但是实现并不是最优的,比如上面的搜索次数可以降低。书中提到,可以通过改进上界和下界来提高搜素效率(这也正是程序中使用上界和下届的方法),如何改进呢?
 
关于上界的分析:如果对于烙饼每次都需要翻转两次才能将最大的烙饼放在最低端,那么n个烙饼最多需要2(n-1)次就可排序成功。将上面程序的实现中上界改为(nCakeCnt - 1) * 2,之后运行,结果如下:

很明显搜素次数下降了400多次!!!!可见修改上界和下届可以很好的降低搜素次数。
 
使用一个测试用例自己来分析一遍上面的 算法,发现有许多无用的翻转。据作者说有一些这方面的论文,查找了一下确实发现了一些,有空研究一下。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值