永不言弃—《Slumdog Millionaire》

 最初是准备发第七篇技术连载,突然想起由于这几天长期沉浸《Friends》,自己很久都没有认真看一部完整的电影了。

 随意在mtime上翻了一翻新片海报。现在回想起来那几乎与普通海报无异,顶部印着电影在多伦多电影节上取得的荣誉,以及转载Time的一句话点评。但影片独特的构思就体现在正下方,一个单项选择题:

what does it take to find a lost love?

a.MONEY     b.LUCK     c.SMARTS     d.DESTINY

 起初是好奇,观赏完全片后便不由得被原著《Q&A》的作者所折服。

 影片的画面开始于一个电视节目《who wants to be a millinaire?》的现场,开始感觉像是小时候喜欢看得国内一出同题材的电视节目—尽管后来愈觉得这类节目缺乏吸引力。结果还没太看明白,画面又切入审讯室里,同样的男主角承受着酷刑逼供。事实上后来全片所体现出的剪辑思路完全突破了传统的框架,产生了令人意料之外的效果。

 影片前半部分故事发在孟买的贫民窟里,有点像曾经看过的一部电影《上帝之城》,事实上最初的感觉是两部影片极其相似—贫民窟里发生的令人心酸的丑恶,以及男主角一颗宁静而执着的心。然而影片的叙事手法却是凭借着电视节目里主持人提出的问题—当然,“现在”已经是不幸而又万幸的主角坐在节目录像前向调查警官“坦白罪行”了。

 从贫民窟走出来以后,影片的线索就随着主角对latika的苦苦追寻而展开。主角的哥哥salim也就开始为人所留意,其实之前有一段salim偷拿jamal心爱的明星签名照去卖钱的情节,但并没有引起我太大注意。直到兄弟俩逃出魔窟的当晚,salim不顾一切地拯救弟弟,却面带阴笑地松开了latika的手,对和谐电影的一贯做法使人隐约感觉到了salim悲剧性的结局。

 弟弟对latika的感情发展依照这样一条线索,在一次大规模的种族宗教械斗中两人共同丧失了双亲,事后主角对latika产生了同病相怜的感情—此时两人互相结识。不幸落入魔掌之后,哥哥的凶狠受到了犯罪分子的怂恿,而最终逃脱使主角对latika的不幸感到万分懊悔,历尽艰辛两人生存了下来,出于对latika的依恋弟弟执意再次回到孟买。salim在这里枪杀了犯罪集团的头目,并与弟弟一道救出了latika,然而此时几乎疯狂的salim投靠了贫民窟的黑老大,并成为其手下一名打手。此时的salim显然对刚刚成年的latika打起了主意,为此不惜拔出手枪将弟弟赶出了旅店。

 影片最巧妙的地方位于片尾,事实上前面已经有多处伏笔显示jamal将利用游戏规则“打电话”的方式与latika通话,此时的jamal深知自己将不再失去,于是毫不犹豫地“乱”选了一个答案。导演选择了“正确”作为最终解答,并让主角抱得两千万与美人同归。

 怪不得海报中的影评里写道“A bouyant hymn to life…”,这确实是一出喜剧,哥哥最终完成了教科书般的自我救赎。剧末众人在维多利亚火车站跳起滑稽的印度舞蹈,网上有评论说本部分毫无存在的价值—但仔细想想,导演和编剧所要向人传递的一种贫民窟人民对生活的乐观,这不仅仅是影片所体现的对贫民窟人民由衷的敬意,同时也是教化大众所要秉持的一种生活态度。

 影片的主题似乎是Destiny,命运,但更像是升华着一种永不言弃的精神,童年的“弟弟”在当今世界里又有多少?在高楼上重逢后,兄弟俩有这样一段对白:

jamal:Salim…where’s latika?

salim:Still?……

 哥哥对弟弟的衷情感到有些不可理解,但幸运的是,这段感情最终得到了所有人的认可—可能除了被salim杀掉的黑老大。

Comments

假期连载之六

限定性线性表——队列,一个初级迷宫解法的讨论及延伸B

 

       本节我们介绍另外一个重要的线性表——队列。首先会讨论队列的几种不同的存储结构,以及相关结构的基本运算问题,最后,我们会对连载五中介绍的迷宫Maze解法进行第一步优化工作。

      

队列Queue

 

与栈相类似,队列也是一种限定存储位置的线性表。它只允许在表的一端插入,在另一端删除。通常把允许插入的一端称为队尾Rear,而把允许删除的一端称为队头Front。队列所具有的这种特性就被称为先进先出First in First out

首先给出队列的抽象数据类型定义:

ADT Queue

{

       InitQueue(&Q);           //队列初始化

       IsEmpty(Q);                 //判空操作,为空返回TRUE,否则返回NULL

       IsFull(Q);                     //与上相反

       EnterQueue(&Q,x);     //进队操作,在队列Q的队尾插入x

       DeleteQueue(&Q,&x); //出队操作,用x取得队头元素的值

       GetHead(Q,&x);          //取对头元素操作,同样用x取得,成功则返回TRUE,否则返回NULL

       ClearQueue(&Q);              //队列置空操作

       DestoryQueue(&Q);    //队列销毁操作

}

相应地,队列也分为顺序存储和链式存储两种表示形式,但这与其它线性表中所定义的顺序表和链表有所不同,需要注意。

 

循环队列

 

循环队列是一种基于数组的顺序队列。对于数组队列elements[maxSize],我们首先设置其队头和队尾指针frontrear。初始化后front=rear=0。当加入新元素时,先将新元素加入rear所在位置,并使rear1。如果要退出对头元素,应当前将front位置的元素记录下来,并且使front1,这就构成了顺序队列的基本运算原理。

由于顺序队列的特性,其最大空间固定,且随着front递增,其front位置不断进1,逻辑上造成队列所能容纳元素个数的逐步递减。为了能充分利用数组空间,一般将数组的前端和后端连接起来,形成一个环形的表,当front和队尾指针rear进到maxSize-1后,再前进一个位置就自动到0,这就是循环队列circular queue

判断是否进1或为0的方法如下:

front=(front+1)%maxSize;

rear=(rear+1)%maxSize;

同时为了避免当队列满时发生rear==front的情况,还应增加一个判断队列是否已满的条件:

(rear+1)%maxSIze==front;

下面给出C语言描述的队列入队出队操作:

int EnterQueue(SeqQueue *Q,QueueElementType x)

{

       if((Q->rear+1)%maxSize==Q->front)

              return FALSE;

       Q->element[Q->rear]=x;

       Q->rear=(Q->rear+1)%maxSize;

       return TRUE;

}

int DeleteQueue(SeqQueue *Q,QueueElementType *x)

{

       if((Q->rear==Q->front)

              return FALSE;

*x=Q->element[Q->front];

       Q->front=(Q->front+1)%maxSize;

       return TRUE;

}

这里另外给出一个C++队列类中对于一次输出队列中所有元素值的方法函数,其原理是对<<运算符进行了重载,这在需要顺序输出的数据结构算法中经常用到。

friend ostream& operator << (ostream& os,SeqQueue<T>& Q)

{

       os<<”font=”<<Q.front<<”,rear=”<<Q.rear<<endl;

       for(int i=front;i!=rear;i=(i+1)%maxSize)

              os<<i<<”:”<<Q.elements[i]<<endl;

       return os;

}

 

链式队列

 

顾名思义,链式队列是基于单链表的一种存储表示。链式队列的优点在于,由于单链表本身可以设置队头指针和队尾指针,因而其入队出队运算非常方便,同时由于不存在队列满的情况,故实际应用上比循环链表的性能更好。在一些需要多个队列的程序中,大多数使用链式存储方式,相较循环队列而言将大大节约系统资源。

由于链式队列的代码与循环队列、单链表操作较为相似,因此这里就不再详述。

 

OSprocess scheduling机制中我们了解到,系统对于就绪队列的操作方法有很多种,并不限于FCFS形式,例如SPF。这说明在一些高级别算法中,产生了一种区别于传统意义上的队列,有时统称为优先级队列Priority Queue。这种队列的应用同样非常广泛。

优先级队列可以为最大优先级队列,也可以是最小优先级队列,以需求不同而论。其一般原理是,当队列初始化后,新元素首先放至队尾,然后调用调整函数。调整函数是一个从队末向队首递增的迭代算法,旨在比较当前元素与新插入元素的优先级大小,如果新元素优先级较高,则与当前元素互换位置,类似于一次冒泡排序。由于算法涉及到了循环操作,其时间复杂度达到了O(n),为了进一步提升算法性能,今后我们会介绍一种最高效的优先级队列存储结构“堆”,它将使类似算法的性能有较大提高。

 

双端队列

 

我们在连载四中给出了双端栈Destack的概念,其目的主要是为了解决一个程序中应用两个栈时存在的空间利用问题。这里介绍的双端队列Double-ended Queue,与最初引入双端栈的目的并不一致,主要是为了提供一种算法更为灵活的存储结构。但了解双端队列后可以知道,双端栈有时也是双端队列的一类特例。

双端队列,即队列本身并不拘泥于FIFO规则,有可能为两端均能执行插入、删除操作,也有可能插入、删除操作在一端执行、另一端只执行插入操作。这种形式对于栈、一般队列来说灵活的多。

       但是,在实际应用中我们会发现大量类似于栈或者队列的存储结构,却很少能碰到有双端对列类似要求的结构,因此其实际利用度并不十分高。尽管如此,这并不影响双端队列被列为第三种限定性线性表结构,这也是引入本部分的目的。

 

       初级迷宫Maze解法的优化讨论

 

       现在我们回顾上节内容中介绍的一个初级迷宫解法。在上一节我们介绍了在解决简单二维迷宫时一般用到的两种算法——递归回溯和栈回溯。我们知道,栈回溯相较于递归回溯而言算法性能上得到了提高,并且在智能化扩展上也有了很大进步。但是我们同样意识到,由于算法的原理是在不断探索中,一旦发现终点则退出,就可能产生解出的路径是否为最优解的问题。

       事实上连载五中程序结果显示的例子证明,其最终路径并不是路程最短的解。

       为了解决这一问题,通常需要在发现终点后,要根据起点和终点及其相互联系区域之间所存在的关系而定。

       1     1     1     1     1     1

       0     0     1     1     0     1

       1     0     0     1     1     1

       1     1     0     1     0     1

       1     0     1     0     0     0

       1     1     1     1     1     1

       首先我们假定起点Maze[1][0]的值为2,把与起点相邻的方格标记为3…依次类推,直到探索出终点位置,为此我们得到以下矩阵:

       1     1     1     1     1     1

       2     3     1     1     0     1

       1     3     4     1     1     1

       1     1     4     1     6     1

       1     5     1     5     6     7

       1     1     1     1     1     1

       根据以上数据可知最优解的路径总数为7-2=5,此时我们从终点出发,选择相邻小于1的元素作为路径点,并按此规律回溯到起点。最终所能得出的路径即为最优路径。

       值得注意的是,由于规则和迷宫形态的不确定性,有时可能会出现多条最优解的情况,但这恐怕也就说明了该迷宫的复杂度较低吧。

       这里选择队列作为本算法的存储表示,那么除了要参考栈的迭代回溯算法的原理之外,另需构建一个双循环回溯算法,下面仅提供该部分内容:

       for(j=PathLen-1;j>=0;j–)

       //nbr为下一个移动的目标元素,offsets[]为前进方向表,here为当前位置,grid为保存探索中已经过元素的矩阵

{

              for(i=0;i<NumOfNbs;i++)

              {

                     nbr.row=here.row+offsets[i].row;

                     nbr.col=here.col+offsets[i].col;

                     if(grid[nbr.row][nbr.col]==j+2)

                            break;

}

here=nbr;

}

 

这里我们仅介绍一种简单的迷宫最优解的解法,并不涉及到高级算法设计中关于最优解的讨论,但由于本部分内容实际上为ACM的重要组成部分,因此在今后的连载中我们将予以详细说明。

 

(未完待续)

Comments

假期连载之五

(迷宫Maze的魅力——一个初级迷宫解法的讨论及延伸A

      

       现在,我们根据以上连载中的内容来研究一个有趣的应用问题。

       假设在一个二位数组maze[m+2][p+2]表示的迷宫中,除maze[1][0]以及maze[m][p+1]分别表示入口、出口外,包括第0m+1行、第0p+1列的元素均表示迷宫的围墙,用1表示,其它元素同样用1表示围墙,0表示通路,如下图例:

       1     1     1     1     1     1

       0     0     1     1     0     1

       1     0     0     1     1     1

       1     1     0     1     0     1

       1     0     1     0     0     0

       1     1     1     1     1     1

       以上是一个6*6的迷宫图示,其中(1,0)(4,5)表示了迷宫的出口和入口。对于迷宫中除边界外的任意元素maze[i][j],有0<i<m0<j<p,并且当maze[i][j]==0时路通,maze[i][j]==1时不通。我们假设迷宫中的个体一次可移动八个方向——NNEESESSWWNW,其数据关系据此可知。我们先根据可前进的方向建立一个前进方向表,给出各个方向的偏移量。

       struct offsets

{

       int a,b;          //ab分别表示在xy方向上的偏移量。

       char *dir;       //dir为方向。

};

offsets move[8];   //各个方向的偏移量表。

在使用上述数据结构时,需要初始化move[8]中8个方向的元素值,如果该前进方向走不通,则在前进路径上回退一步,再尝试其他的允许方向。

同时为了防止重走原路,另外设置一个标志矩阵mark[m+2][p+2],它的所有元素都初始化为0,一旦进行到迷宫的某个位置[i][j],则将mark[i][j]置为1,下次这个位置则被屏蔽。下面给出解决迷宫问题的递归算法C++语言描述:

int SeekPath(int x,int y)

//在本例中我们首先从Maze[1][1]开始探索,如果找到出口则返回1,否则返回0;

{

       int i,g,h;

       char *d;                                                          //g,h记录位置信息,dir记录方向

       if(x==n&&y==p)

              return 1;

       for(i=0;i<8;i++)                                               //每次选择一个方向探索

       {

              g=x+move[i].a;

              h=y+move[i].b;

              d=move[i].dir;

              if(Maze[g][h]==0&&mark[g][h]==0)

              {

                     mark[g][h]=1;

                     if(SeekPath(g,h))                             //选择此位置递归探索

                     {

                            cout<<”(”<<g<<”,”<<h<<”),”<<”Direction ”<<dir<<endl;

                            //此处输出路径的顺序是逆向的

                            return 1;

}

}

}

if(x==1&&y==1)

       cout<<”no path in Maze”<<endl;

return 0;

}

我们将上述迷宫数据经由此算法检查,g++给出了如下输出:

 

(4,4),Direction W

(4,5),Direction SE

(3,4),Direction NE

(4,3),Direction SE

(3,2),Direction S

(2,2),Direction SE

(1,1),Direction E

 

由于算法本身的问题,输出逻辑上存在一些偏差,但并不影响其正确性。

现在,我们对递归算法作出一些改进,使其能通过栈处理的方式提高算法性能。为了达到这一目标,我们需要设置一个三元组来存储已走过路径的信息,如下所示:

struct items

{

       int x,y,dir;             //位置和前进方向的序号

}

此算法的原理是,我们对当前位置的信息进行记录并压栈,按照前进方向表给出的数据向某一个允许的方向前进一位,并将此位置压栈,继续进行上述过程。如果当前位置无法走通,则将前进方向退栈,并在前进路径上回退一步,并尝试其它方向,如果栈空则表示已回退到开始位置。

下面给出迷宫非递归算法的C++描述,需要提前说明的是,我们已假设存在一个完整的Stack类,并具备连载四包含的所有栈操作函数。

int path(int m,int p)                    //关于参数的介绍请借鉴上例

{

       int i,j,d,g,h;

       mark[1][0]=1;                            //假定(1,0)为入口

       Stack<items>st(m*p);         //初始化栈st,其大小为m*p,这是根据迷宫空间设置的最大可能值。

       items tmp;                          //选择当前位置信息

       tmp.x=1;

       tmp.y=0;

       tmp.dir=2;

       st.Push(tmp);                            //将初始位置压栈

       while(st.IsEmpty()==FALSE)

       {

              st.Pop(tmp);                //将栈顶位置信息弹出,并设置为当前位置

              i=tmp.x;

              j=tmp.y;

              d=tmp.dir;

              while(d<8)                   //选择前进方向

              {

                     g=i+move[d].a;

                     h=j+move[d].b;

                     if(g==m&&h==p)

                     {//已找到出口

                            cout<<st;      

//这里在Stack类的内部重载了操作符<<,用于输出st中保存的路径

                            cout<<m<<” “<<p<<endl;

                            return;

                     }

                     if(Maze[g][h]==0&&mark[g][h]==0)

                     {//选择新的可通位置并前进

                            mark[g][h]=1;

                            tmp.x=i;

                            tmp.y=j;

                            tmp.dir=d;

                            st.Push(tmp);

                            i=g;

                            j=h;

                            d=0;

                     }

                     else d++;

              }

              cout<<”no path Maze”<<endl;

       }

};

在上述算法中,由于涉及到了栈的操作,可能会使算法难度增加,但事实证明其在性能及智能化的扩展方面存在很大的提高。对于小型、简单的二维迷宫,均可根据以上算法对其进行有效的处理。

在今后涉及到树或图部分的内容时,我们将再次回顾迷宫Maze问题,届时的迷宫实例将具备更多复杂的元素以及更巧妙的规则,使当前的算法复杂度大幅提高,也不仅仅是一般数据结构或算法就能够轻松解决的。

 

(未完待续)

Comments

假期连载之四

限定性线性表——栈,栈的递归应用

      

五)限定性线性表——栈和队列

 

       前面我们介绍了几种简单线性表的抽象数据结构及其基本运算,在一些应用中,通常还有一些逻辑结构与线性表相同,但含额外运算限制的数据结构需求,例如栈和队列。

      

       Stack被广泛应用于计算机基础应用中,例如汇编程序中的中断、句法识别以及表达式运算,函数的传参、函数值返回等各种机制。

       栈的定义是一种只允许在表的末端进行插入和删除的线性表。通常允许相关操作的一端称为栈顶Top,而不允许进行插入和删除操作的一端称为栈底bottom。当栈中没有任何元素时称为空栈。由栈的定义可知,栈又是一种后进先出(LIFOLast In First Out)的线性表。

       栈的抽象数据类型定义与普通线性表相类似,例如下面的C语言形式描述:

       ADT Stack

{

       数据元素:可以是任意类型的数据,但必须属于同一个数据对象。

       结点关系:栈中数据元素之间是线性关系。

       InitStack(S);          //初始化空栈

       ClearStack(S);              //S置为空栈

       IsEmpty(S);           //判断是否栈空,是则返回TRUE,否则返回FALSE

       IsFull(S);               //判断是否栈满,与上类似。

       Push(S,x);             //S栈顶压入元素x,成功返回TRUE,否则返回FALSE

       Pop(S,x);                     //S栈顶弹出元素,并保存入x返回,与上类似。

       GetTop(S,x);         //S栈顶元素并保存入x返回,但不弹出该值,与上类似。

}

通常对于一种线性表,一般具备顺序存储和链式存储两种形式,栈相应分为顺序栈和链栈。

顺序栈按定义可知其是建立在顺序存储结构的基础上,通常用一维数组表示。下面是顺序栈的结构类型定义:

#define Stack_Size 50

typedef struct

{

       stackElementType elements[Stack_Size];

       int top;

}

C++描述的顺序栈初始化构造函数如下:

template <class T>

seqStack<T>::SeqStack(int sz):top(-1), Stack_Size (sz)

{

       elements = new T[Stack_Size];

       assert(elements != NULL);    //判断动态存储分配是否成功

}

上段程序需要注意的是,构造函数在声明部分就初始化了数据成员的值,并在参数部分声明了一个局部变量,这里的Stack_Size并非const类型,这可以使用户自己定义顺序栈空间的大小,top=-1表示栈顶指针指空,这种写法需要注意

下面以链栈为例,给出栈的几种基本运算的伪码描述。

1)进栈操作

       Push(top,x)                                       //top为栈顶指针,x为数据元素值,下同

              temp=(LinkStackNode *)malloc(sizeof(LinkStackNode));

              if temp==NULL then

                     return FALSE;

              else then

                     temp->data=x;

                     temp->next=top->next;

                     top->next=temp;

                     return TRUE;

2)出栈操作

       Pop(top,*x)

              temp=top->next;

              if temp==NULL then

                     return FALSE;

              else then

                     top->next=temp->next;

                     *x=temp->data;

                     free(temp);

                     return TRUE;

       在以上两例中,栈由带头结点top的单链表实现,具体操作时采用了头插法入栈。实际应用中还可以采用尾指针进行尾插法入栈。

3)多栈共享

       在实际应用中,可能会遇到一个程序使用多个栈的情况。当使用顺序栈时,会由于栈空间预估不足,造成有的栈溢出、有的栈空闲过多等问题,为了解决上述问题,通常采用多个数组共享一个数组空间,并利用栈操作的特性对其存储空间互相补充,这就是多栈共享技术。

       多栈共享中又存在两种情况,其一是程序需要使用最多两个栈的情况,通常采用双端栈技术予以解决,这种栈的特点如下:

1、  首先申请一个数组空间S[M],将两个栈的栈底分别放在一维数组的两端,分别为0M-1

2、  通过简单对比可知,由于栈顶动态变化,两栈在空间上形成互补关系,逻辑上实现了栈空间由其所需求空间动态决定的机制,从而提高了空间利用率。

3、  在实际应用中通常将两个栈编号标记,方便调用。

另一种情况,是当程序需求两个以上栈的情况。显然双端栈无法满足这一需求。因此当我们遇到这种情况时,一般采用链栈形式,并用一个统一的一维结构指针数组管理每个栈的栈顶指针。

 

栈与递归

 

递归recurve在数学和计算机科学中有着十分重要的作用。在计算机科学中,递归从编译原理的句法定义,到数据结构中的树形结构搜索、排序等问题,都是必不可少的应用。

递归在数学及程序设计方法学中的定义是:若一个对象部分地包含它自己,或用它自己给自己定义,则称这个对象是递归的,而且若一个过程直接地或间接地调用自己,则称这个过程是递归过程。

 有人可能注意到了“直接”或“间接”的表述,“直接递归函数”顾名思义就是在自己的定义中直接调用自己的方法函数。“间接”主要是指方法函数通过一系列的中间语句,通过其它函数调用自己。

一般在以下3种情况下,需要使用递归方法。

1)定义上是递归的

       例如数学上常用的阶乘函数、幂函数、斐波拉契数列等,其定义运算都是递归的。例如斐波那契数列,这是一个从前两项为自然数1,第三项开始为前两项之和的无穷数列,即:

11235813n。在计算机实现时,我们只需给出此数列的递归关系“f(n)=f(n-1)+f(n-2),其中n>=2,即可实现递归运算。

2)数据结构是递归的

       在我们已经讨论过的单链表结构中,可以明显地看到一种递归关系。即LinkNode由数据域data和指针域next构成,而next又由LinkNode定义。利用递归关系我们可以写出寻找单链表中某一结点的算法的递归形式。

3)问题的解法是递归的

       递归是解决一些应用问题唯一有效的解决方案。典型例题即是著名的汉诺塔Tower of Hanoi问题。问题介绍可以看wiki

http://zh.wikipedia.org/w/index.php?title=%E6%B1%89%E8%AF%BA%E5%A1%94&variant=zh-cn

       这里给出简单的伪码演示:

       Hanoi(n,A,B,C)

              If n==1 then printf Move top disk from A to C;

              Else then

                     Hanoi(n-1,A,C,B);

                     printf Move top disk from A to C;

                     Hanoi(n-1,B,A,C);

       这里重申递推和递归的关系,递推是利用问题本身所具有的递推关系对问题求解的一种方法,而这种递推性质决定了已知i-1的解,由此可求得12i-1的一系列的解,其问题规模为i,例如n!问题。

       递推问题通常可以用递归方法求解,同时也可以使用循环迭代的方法求解

 

       在程序设计语言中,递归的实现主要得益于递归工作栈的工作原理。每一层递归调用所需要保存的的信息包括:

1)返回地址,即上一层中本次调用自己的语句的后继语句处。

2)在本次过程调用时,为与形参结合的实参创建副本。

3)本层的局部变量。

在每进入一层递归时,系统就要建立一个新的工作记录,把上述项目登入并加入到递归工作栈的栈顶位置。每退出一层递归,就从递归工作栈退出一个工作记录。

 

在实际应用中,递归通常体现出两个特性:

1)递归算法简单有效,通常可用来解决一些复杂问题。

       2)由于实现机制的问题,递归算法效率较低。

       因此在求解一些问题时,我们一般用递归方法分析问题,而使用非递归方法求解相关问题。

1)用栈实现递归过程的非递归算法

       一般的递归过程可以用递归调用树进行表示,因此可以由栈代替递归算法实现树的运算,这将在以后树结构的介绍中作一说明。

       2)用迭代法实现递归过程

       实际上就是使用循环结构,有一类递归可以使用循环结构简单表示,即单向递归。单向递归是指递归函数中虽然有一处以上的递归调用语句,但各次递归调用语句的参数只和主调用函数有关,相互之间参数无关,并且这些递归调用的语句处于算法的最后,例如斐波那契数列。还有一种递归,指递归调用语句只有一个,而且是处于算法最后,显然这是单向递归的一个特例,称为尾递归。

       无论使用何种非递归方法实现递归过程,一般应先根据递归算法画出程序流程图,然后建立起循环结构。

 

       在下一节,我们将继续扩展递归应用,使用回溯法backracking解决较复杂的迷宫Maze问题,并对几种高级编程技术进行介绍,并加以综合利用。

 

(未完待续)

 

Comments

假期连载之三

(线性表的链式存储—循环链表、双向链表、静态链表)

 

上节我们介绍了线性表的链式存储结构中最重要的一种—单链表结构,但在这样的结构中也存在一些问题,而这些在顺序存储中却能得到较好的体现。为此,我们需要对单链表的存储结构进行一些改进,以实现我们需要的功能。

例如,在单链表中有时会给出某个结点的地址,并要求寻找其它所有结点,我们知道单链表结点中包括了指向其直接后继的指针域,但并不能回溯执行,因此在大多数情况下并不能实现题设的要求。

我们提供一种“首尾相接”的链表,其原理是将尾结点的next指针由null改为指向表头结点,于是就形成了单链表形式的循环链表,简称循环链表。

在不带附加头结点链表中,尾结点的情况为rear->next=head,其中head为头指针。而在带头结点链表中这种情况就变成rear->next=head->next。相应地,关于单链表操作的循环条件p!=NULLp->next!=NULL将变为p!=headp!=head->next,具体由是否附加头结点决定。

下面来看一个关于循环链表的应用问题——约瑟夫环(Josephus)。

 

约瑟夫环问题原本为一个数学应用,是由古罗马的史学家约瑟夫(Josephus)提出的。它的具体数学抽象为:设有编号为12,……,nn(n>0)个人围成一个圈,从第1个人开始报数,报到m时停止报数,报m的人出圈,再从他的下一个人起重新报数,报到m时停止报数,报m的出圈,……,如此下去,直到所有人全部出圈为止。当任意给定nm后,设计算法求最后一个出圈的人的最初序号。

事实上数学家们早已给出了这个问题或以此为基础衍生出的各种问题的数学解答,我们仅需要根据简单的递推公式即可轻松解决此类问题。但事实上,它其实也是一个循环链表的典型问题。

       这是因为,其一,环事实上就是循环链表的抽象表现,在本题中环链的大小不断发生改变,也恰符合链表设置的最初意义,另外,约瑟夫环所对应的操作几乎囊括了循环链表的所有基本运算问题。

下面给出C++描述的约瑟夫环求解代码:

template<class T>

void Josephus(CircList<T>&Js,int n,int m)         //n为结点数,m为间隔数

{

       CircLinkNode<T>*p=Js.getHead(),*pre=NULL;

       int i,j;

       for(i=0;i<n-1;i++)

       {

              for(j=1;j<m;j++)

              {

                     pre=p;

p=p->link;

}

cout<<”出圈的是:”<<p->data<<endl;

pre->link=p->link;

delete p;

p=pre->link;

}

};

       注:本例仅给出约瑟夫环的求解方法函数,CircLinkNodeCircList类需要自行写出。

 

循环链表实现了一定的回溯操作,丰富了单链表的功能。但其在寻找直接前驱时的时间复杂度仍为O(n),我们要使用更为高效的替代算法以实现O(1),则就产生了双向循环链表,简称双向链表。

顾名思义,双向链表的结点中包含了三个数据成员,除原有的数据域,直接后继指针*next外,又添加了指向直接前驱的指针*prior

在实际应用中,双向循环链表在仅需求后继结点的情况下和单链表的基本用法相类似。但当需同时顾及到前驱和后继两方面结点时就会发生改变。

 

首先我们知道在双向链表中存在下式成立:

p->prior->next==p;

p==p->next->prior;

 

我们据此给出双向链表前插操作的伪码表示:

DlinkIns(L,i,e)                                          //L为原链表,i为插入位置,e为插入数据

       //检查位置i是否合法,否则返回-1

       //设置*p指向第i个结点

       S=(DNode*)malloc(sizeof(DNode));

       If(s) then

              s->data=e;

              s->prior=p->prior;

              p->prior->next=s;

              s->next=p;

              p->prior=s;

              return OK

       else then

              return -1

 

执行循环链表删除运算时,需要注意应从两个方面同时补链,例如:

p->prior->next=p->next;

p->next->prior=p->prior;

 

通过连载一和连载二我们较详细地介绍了线性表的几种存储结构和运算方法,它们都是基于C/C++Java语言而言的。在一些语言诸如BASICFORTRAN中并不包含“指针”这种数据类型(当然Java中也不存在C语言意义上的指针,但并这不影响其对链表结构的诠释)。

      

       在我们同样需要在此类领域中实现链表应用时,数组就成为最为可靠的手段,但这并不同于顺序表的动态存储,而是采用结构体数组的形式,通过设置数据域和“游标Cursor”模拟链表结点,以达到和链表类似的目的,这就是静态链表

       游标Cursor通常为非负整数形式,它存放后继结点在结构数组中的数组下标值(并非绝对位置)。根据结点cursor值可以轻松凭借StaticList[cursor].data获得当前后继结点的值。而数组最后一个结点的cursor值通常为-1表示null

       由于数组的特性,首先静态链表的长度是固定的。因此在执行添加/删除操作时并不能实际创建/释放内存空间,而全部在原数组内部进行。

       在后插法添加结点操作中,新结点先存放在数组空闲空间中,然后使新结点的cursor值等于要插入结点的cursor,再将原结点的cursor值等于新结点在结构体数组中的实际下标值。以上步骤可由以下语句实现:

       StaticList[s].cursor=StaticList[p].cursor;

       StaticList[p].cursor=s;

       这说明尽管静态链表中的数据结点物理位置可能并不连续,但其逻辑次序并未发生变化,这就接近于链表的实际存储结构。

       执行静态链表的删除操作时,只需使要删除结点前驱的cursor值等于要删除结点的cursor值即可。实际上要删除的结点空间并未发生任何修改。

       由以上操作可知,当静态链表在经过数次插入/删除操作后,其总结点数其实只增未降,这就使得静态链表的运行效率大打折扣。

       为此,我们一般额外设置一个备用静态链表,该表结点所存放的值是空闲空间以及因删除操作而回收的结点空间,实际运算时需要原链表和备用表的互相协作以达到相关目的。

 

未完待续

Comments

己丑寄语

 最近开始看《Friends》,曾经在网上听说了Ross的十年之痒,但苦于一直没有充足的时间领略这部长达238集并热播十年的情景剧。现在终于能开怀畅“品”了。

 更夸张的是,第一次接触Friends就一口气用一天多点的时间看完了第一季的完整内容…或许还真是闲得慌了。不可否认,Friends所能给人带来的轻松与愉悦的确名副其实,片中体现出的浓厚的美国文化则深深吸引了观众。这也使得我对情景剧一贯不以为然的态度发生了转变。

 除夕夜前半段和家人一起看电视,后半段就自己继续“品”着Friends。很快,六十年一回归的己丑年将拉开大幕,“努力奋斗”这份来自朋友的祝辞,也将成为这一年的永恒主题。

Comments

假期连载之二

       线性表的基本运算算法分析线性表的链式存储—单链表

前面我们介绍了线性表的顺序存储中有关数据结构以及其常用运算,接下来我们将对其中涉及搜索、插入和删除部分的代码进行性能分析。

      

       首先对于上节使用的伪码描述的Locate(L,e)搜索算法进行分析,该算法的核心思想是,从表L的开始位置起,根据给定的值e,逐个与各表项的值进行比较,若给定值与表中某个值相等,则算法报告搜索成功,并输入该表项的具体位置,如果查遍所有表项仍不符合要求,则返回null通常衡量搜索代码的时间代价时使用数据比较次数的数值,在搜索成功的情况下,若要找的正好是表中的第一项,则搜索比较次数为1,若为最后的第n项,则比较次数为n,这是最坏的情况,因此若要计算平均数据比较次数,需要考虑到各个表项的搜索概率pi以及找到该表项时的数据比较次数ci。因此搜索的平均数据比较次数ACN(Average comparing number)为:

                                          ACN=pi*ci  (i=1,2,3,…,n)

    通常我们仅考虑搜索项中相等概率的情形,即每个表项被选择的概率均为1/n,且搜索第一个表项的搜索比较次数为1,之后依次类推。则:

            ACN=pi*ci=(1/n)(1+2+,…,+n)=(1/n)*(1+n)*n/2=(1+n)/2

即需要比较(1+n)/2个表项。

    相反,如果搜索不成功即整个表都检测一遍,因此ACN将为n

 

    在执行顺序表的插入或删除运算时,需要注意,此时表的长度是会发生改变的,因此为了在执行这一类操作过程中不改变原表项的位置关系,通常需要成块移动表项。例如在插入运算中,必须先将第i个到第n个所有表项成块后移一位,以空出可供插入新表项的位置。而当删除操作时,在删除第i个表项后,需要将从i+1n-1的所有表项向前移动一位,覆盖原位置。在这两种操作中,表项移动的方向是恰好相反的

   

    因此在分析顺序表的插入和删除操作时,其时间代价主要是以循环内表项的移动次数来衡量的

 

    例如InsList(L,i,e)插入运算,在插入数据之前,必须先从后往前循环,将第n项到n-i项均向后移动一位。因此最好的情形是在第n+1位插入新表项,此时表项移动个数为0。最差情况是在第1个位置插入新表项,即在0之后。移动表项个数为n。因此平均数据移动次数AMN(Average moving number)在各表项的插入概率相等时为:

                     AMN=(1/(n+1))* (n-i)=(1/(n+1))*(n+…+1+0)=n/2(i=1,2,…,n+1)

因此就整体性能来说,插入时有i+1个插入位置,平均移动n/2个表项。

   

    同时对于删除操作而言,当删除第i个表项时,需要移动第i+1到第n个总共n-1个表项。另外对于插入操作所不同的是,删除操作并不涉及到第n+1项的情况,因此上式AMNi的情况应为1,2,…,n即可,所以:

            AMN=(n-1)/2

       当然在一些算法中并不要求插入时并不必须在表末进行,或者删除时可以将最后一个表项填到第i个空位中去,则此时的插入AMN固定为0,而删除AMN固定为1

 

2、  线性表的链式存储(Linked List)

在线性表的顺序存储中,我们使用了基于数组的存储结构,即其物理位置上为邻接关系,这使得顺序表具有如下优点:

1)  无需为表示结点间的逻辑关系而增加额外的存储空间,存储利用率提高。

2)  可以方便地随机存取表中的任一结点,存取速度快。

但是,这也不可避免地带来的一些缺点:

1)  在表中插入新元素或删除无用元素时,为了保持其他元素的相对次序不变,平均需要移动一半元素,运行效率较低。

2)  由于顺序表要求占用连续空间,我们在Operating System中了解到,当内存空间使用Static allocation时,当表的长度变化较大,则难以确定合适的存储空间大小,容易造成内存空间的浪费或溢出。如果采用Dynamic allocation策略使用动态存储模式,则可以使用新数组替换原数组,尽管在空间上能够得到有效利用,但会造成较大的时间开销。

 

为了克服顺序表的缺点,在一些插入/删除操作频繁从而导致空间需求不定的情况中,我们采用链接方式来存储线性表。

 

从链表的实现角度来看,链表分为静态链表动态链表两类。而从链接方式来看,链表分为单链表循环链表双向链表三类。

 

       在链表结构中,其存储表项可以是物理连续的,也可以是非连续的,甚至呈零散分布,故其结点的逻辑次序和物理顺序不一定相同。为了正确表示出单链表的逻辑关系,我们必须在存储每个表项数据元素值的同时,存储指示其后继结点的地址信息,我们把这两部分信息组成的存储映像称为结点(Node),通常也称为域(Field)

       结点包含了两个域:数据域和指针域,他们分别用来结点的值和直接后继结点的地址。在单链表Single linked list中,每一个结点中只有一个next指向其直接后继。

 

       值得注意的是,我们在构建一个链表时,为了存放第一个结点的地址,因此通常设置一个头指针H指向第一个结点并作为其直接前驱,而最后一个结点的next指针直接设置为空null

 

下面我们给出C语言方法描述的单链表存储结构:

 

       typedef struct Node

{

       ElemType data;

       struct Node *next;

}Node,*LinkList;                  //这里的LinkListNode *等价,通常使用LinkList强调头指针,而Node*来定义指向任意结点的指针。

 

C++的面向对象方法中,我们通常使用两个类即结点类和链表类来定义单链表。而其具体实现的方法又有4种。

1)      复合类,在LinkNode类中声明友元类List,从而使得LinkNode类和List类都能访问LinkNode类的私有数据成员。

2)      嵌套类,在List类定义的内部对LinkNode类进行定义,则必须将LinkNode类定义在其private部分,从而确保List类的外部对象和函数不能直接接触到LinkNode类的对象,但LinkNode类的数据成员则必须放在public部分,使得LinkNode类和List类的成员都能访问它们。

3)      基类和派生类,把LinkNode声明为基类,同时List类声明为派生类,建立继承关系,让List类继承其数据成员和某些成员函数,表示一种使用关系。此时LinkNode类的数据成员应为protected,以保证被子类使用。

4)      struct定义LinkNode类,和C语言方法相似,但其失去了封装性,仅在需要简化描述时采用此类方法。

下面仅给出复合类的具体声明方法:

class List;

class LinkNode

{

       friend class List;

       private:

              int data;

              LinkNode *link;

};

class Link

{

       public:

              //链表公共操作

              ……

       private:

              LinkNode *first      //链表头指针

};

 

单链表的基本运算

 

       1)初始化单链表:

              InitList(LinkList *L)

{

       *L=(LinkList)malloc(sizeof(Node));

       (*L)->next=NULL;        //带头结点的链表,故L->next=NULL

}

 

这里明确malloc/freenew/delete的区别,我们知道,malloc/freeC++/C里的标准库函数,而new/deleteC++的运算符,两者都含有申请内存空间/释放的功能,但malloc/free仅仅能做到这一点。C++引入运算符new/delete的目的是,当我们需要动态地建立对象时,由于面向对象方法的限制,必须要利用到运算符的性质,而避免使用函数来操作。在分配内存空间后,还必须执行对象的构造方法,在释放空间时需要执行析构函数,这是malloc/free所不具备的,因此new/delete有其合理性,这一点在java中得到了继承和发展。

 

       2)建立单链表

       这里先介绍头结点的概念,我们知道,通常链表初始化过程中会产生一个头指针*L,此时L直接指向null,有时我们需要建立一个头结点以满足一些特殊操作,因此L->next被设置为null

             

       基于头结点的建表法,即头插法建表。头插法建表的思想是,在一个带头结点的链表L中,首先申请结点空间*s,并给其数据域赋值。由于L->next指向了表中下一个结点,故先s->next=L->next链接后继结点,同时L->=s完成链表的结点插入操作。其伪码表示为:

       CreateFromHead(L)

              flag=1;

              while flag do

                     c=getchar();

                     if c!=’$’ then                                                   //用来确保’$’为输入结束符

                            s=(Node*)malloc(sizeof(Node));

                            s->data=c;

                            s->next=L->next;

                            L->next=s;

                     else then

                            flag=0;

 

       头插法中输入元素与得到的单链表逻辑顺序相反,因此也被称作逆序建表法。如果需要链表顺序与输入顺序一致,可以直接采用尾插法建表解决。

       CreateFromTail(L)

              flag=1;

              r=L;                                                         //r为尾指针,初始化时应指向头结点。

              while flag do

                     c=getchar();

                     if c!=’$’ then

                            s=(Node*)malloc(sizeof(Node));

                            s->data=c;

                            r->next=s;

                            r=s;

                     else then

                            flag=0;

                            r->next=NULL;     

3)删除单链表

单链表删除操作的思想是,首先声明一个空指针r并使其指向要删除的结点,同时将要删除结点直接前驱的next指针指向其直接后继,然后释放r的空间即可。下面给出一个带头结点链表中删除第i个元素的伪码表示:

DelList(L,i,e)

       pre=L,k=0;

       while pre->next!=NULL&&k<i-1 do

              pre=pre->next;

              k=k+1;

       if !(pre->next) then

               error “删除位置不合理!”;

       r=pre->next;

       pre->next=pre->next->next;

       e=r->data;

       free(r);

       return OK;

 

4)链表的前插操作,和头插法建表类似,但需要注意的是,由于插入操作可在链表尾部进行,因此其循环条件为pre!=NULL&&k<i-1,并非是pre->next!=NULL,如果在删除操作中忽略了这一点,则可能造成空指针操作,发生错误。

 

 

(未完待续)

Comments

假期连载之一

关于本连载的说明请查阅http://www.hanyi.name/blog/?p=118

一、常用数据结构分析(概念、ADTADT实现、线性表的顺序存储)

 

一) 数据即是指描述客观事物的数值、字符以及能输入机器且能被处理的各种符号集合。数据元素是指组成数据的基本单位,是数据集合的个体。相应的,数据结构就是性质相同的数据元素的集合,是数据的一个子集。

数据结构的内容大概包括以下三个方面:

1、  逻辑结构(通常)

     集合结构,结构中的数据元素除了同属于一个集合的关系外,无其他关系。

     线性结构,结构中的数据元素之间存在一对一的线性关系。

     数型结构,结构中的数据元素之间存在着一对多的层次关系。

     图状结构或网状结构,结构中的数据元素之间存在着多对多的任意关系。

2、  存储结构

     顺序映像,即顺序存储结构。

     非顺序映像,即非顺序存储结构。

3、  运算集合

数据结构另一个重要内容即是对一类数据的相关运算操作。

 

       二) 抽象数据类型(Abstract Data Type,通常称ADT),即包括一个相应的数学模型以及该模型上应用的各种运算,它集中体现了程序设计过程中的分解、抽象和信息隐藏等原则。

对于如何实现抽象数据类型,一般有三种方法,这也是目前程序设计的三个重要方法。

1、面向过程的程序设计,根据逻辑结构选择适当的存储结构,并按要求设计出相应的子程序或子函数,一般使用C语言、Pascal语言描述。

2、“包”、“模型”的设计方法,是面向模块结构的表示和实现,一般是指DODAda语言、module-2语言和Pascal语言所支持的方法。

3、面向对象程序设计,借助对象描述数据类型,存储结构和操作函数被放在一个整体结构中,例如C++Java等语言提供了此功能的支持。

 

在今后的连载内容中,我们将采用面向过程与面向对象相结合的方法,即给出两种描述,分别使用C语言和C++语言实现,方便大家在各方面的理解。高级算法部分主要以伪码形式给出,适当利用高级程序设计语言加以说明。

 

) 分别用CC++抽象数据类型实现的举例。

       ADT<ADT>{

              数据对象:数据对象的定义。

              结构关系:结构关系的定义。

              基本操作:基本操作的定义。

}ADT<ADT>

1、  C语言实现

C语言中我们通常使用typedef自定义类型和结构体实现数据类型的集合。例如关于一个“学生”的集合:

typedef struct node{

       char Name[20];

       char Sex;

       int Age;

       float Money;

}Employee,*EmpPtr;

       值得说明的是,typedef同时定义了两个新的数据类型名,Employeestruct node结构体类型名,而EmpPtr指结构体指针类型,通常代替struct node *类型名。

2、  C++语言实现

C++由于其特殊的特性对ADT有多种表示方法,通常使用自定义抽象类、自定义模版和void struct *三种方法,其中void struct *由于模版类的使用已经很少再使用,如有需要读者可以参考C语言的方法使用。

     自定义抽象类,与Java不同,C++中并不提供abstract关键字声明抽象类,在确需要使用到抽象类时,须采用例如virtual void draw() = 0;的函数声明方法,称为“纯虚函数”,拥有“纯虚函数”的类自动为抽象类。

     自定义模版template,目的是使抽象出的对象并不依赖于抽象类的数据类型。下面给出一个抽象类的声明和使用方法:

template<class T>

class Point{

private:

T x;

     T y;

public:

     Point(T,T);

     Point(Point p);

     T get_x();

     T get_y();

     virtual void draw() = 0;

}

int main()

{

     Point<int> a;      

     return 0;

}

 

       ) 线性表

       线性表Linear List是指n个类型相同的数据元素的有限序列,对n>0,除第一个元素无直接前驱、最后一个元素无直接后继外,其余每个元素只有一个直接前驱和直接后继,线性表的抽象数据类型定义如下:

ADT LinearList{

数据元素:D={ai| ai D0,i=0,1,2,…,n,n>=0, D0为某一数据对象};

结构关系:R={<ai ,ai+1>| ai , ai+1D0,i=0,1,2,…,n-1};

基本操作:

        InitList(L);             //初始化L为空表

        DestroyList(L);      //L销毁

        ClearList(L);          //L置为空表

        EmptyList(L);        //如果L为空则返回TRUE,否则返回FALSE

        ListLength(L);        //如果L为空返回0,否则返回FALSE

        Locate(L,e);           //如果L中存在元素e,则将当前指针指向元素e的所在位置,并返回TRUE,否则返回FALSE

        GetData(L,i);         //返回L中第i个元素的值

        InsList(L,i,e);         //L中第i个位置之前插入新的数据元素eL的长度加1

        DelList(L,i,e);        //删除L中第i个数据元素,并用e返回其值,L的长度减1

}ADT LinearList

 

1、  线性表的顺序存储(Sequential List),其实就是指线性表基于一维数组的存储表示,数组占用了内存中一段连续空间,其元素的地址映像计算公式为:

Loc(ai)= Loc(a1)+(i-1)*k

       注:上式表示在一个具有n个元素的线性表中,每个元素占k个单元,第一个元素的地址为Loc(a1),则可通过上述公式计算第i个元素的地址Loc(ai)

 

C语言定义线性表的顺序存储结构的静态存储表示如下:

#define MAXSIZE 100

typedef struct

{

       ElemType elem[MAXSIZE];

       int last;

}SeqList;

相应地,其动态存储表示如下:

typedef struct

{

       ElemType *elem;

       int last,maxSize;                   //利用maxSize可方便控制数组空间的大小

}SeqList;

 

同时,我们也给出C++定义的线性表的顺序存储结构的动态存储模版类表示如下:

#include <iostream.h>

#include <stdlib.h>

#include “LinearList.h”

const int defaultSize = 100;          //这里的const指静态常量,在C++定义中,const成员函数不能调用非const成员函数

template <class T>

class SeqList:public LinearList<T>

{

protected:

       T *data;

       int maxSize;

       int last;

       void resize(int newSize)        //改变data数组空间的大小

public:

       ……                                          //方法函数

}

……

       值得注意的是,在顺序表动态存储表示中,由于数组本身不可能被动态改变其大小,因而实际上是通过创建新数组并加以复制的方法实现的,复制的方法涉及到了C++中数组指针的内容,其和指针数组有明显区别,指针数组的定义方式int *p[10]代表p数组中每一个元素都是一个指针,即*p[0]*p[1]…..数组指针一般定义为int (*p)[10],这里仅表示一个指向数组的指针,它们访问内部元素的形式为而这里小结如下:

       提前说明,数组指针与指针数组在C语言和C++之间也存在一些不同,但由于该名词是由C++中率先提出的,因此一般会按照C++标准来定义这些概念,并解释相关问题。例如在指针数组中,一般读取其中元素的方法是:

       int *p=new int[maxSize];

       cout<<*(p+n)<<endl;                  //输出第n+1个元素的值

而在数组指针中,我们一般用以下方式读取数组元素:

       int (*q)[maxSize]={1,2,3,…,maxSize};

       cout<<*(*q+n)<<endl;                 //同上

 

下面给出C++线性表的顺序存储结构的动态存储模版类中重置大小函数reSize()的方法定义:

template <class T>

void SeqList<T>::resize(int newSize)

{

       if(newSize <= 0)

       {     cerr << ”无效的数组大小” <<endl;

       return;

}

       if(newSize != maxSize)

       {

              T* newarray = new T[newSize];

              if(newarray == NULL)

              {

                     cerr << “存储分配错误!” <<endl;

                     exit(1);

}

int n = last+1;

T* srcptr = data;

T* destptr = newarray;

while(n–)

       *destptr++ = *srcptr++;

delete []data;

data = newarray;

maxSize = newSize;

}

}

       在顺序存储结构中,由于动态存储和静态存储的区别,其内部定义的基本运算也不尽相同,我们将给出部分运算的伪码表示:

      

Locate(L,e)                          //线性表的查找

       i=0;

       while i<=L.last && L.elem[i]!=e

              do i++;

       if i<=L.last

           then return i+1;

       else

              then return -1;

 

InsList(L,i,e)                        //线性表的插入

       if i<1 || i>L.last+2

              Then error “插入位置不合法

       if L.last >= maxSize-1

              Then error “线性表已满!”

       for k=L-last k>= i-1 k—              //从线性表尾部开始移动所有L.last+2-i个元素后继一位

              L.elem[k+1]=L.elem[k];

              L.elem[i-1]=e;

              L.last++;

       return OK

 

DelList(L,i,e)                       //线性表的删除

       if i<1 || i>L.last+1;

              Then error “删除位置不合法

       *e=L.elem[i-1];

       for k=i i<=L.last k++

              L.elem[k-1] = L.elem[k];

       L.last–;

       return OK;

 

Merge(LA,LB,LC)                //线性表的合并,按非递减有序序列合并

       while i<=LA.last && j<=LB.last

              do if LA.elem[i] <=LB.elem[j]

              then LC.elem[k]=LA.elem[i];

                     i++;k++;

              else then LC.elem[k] = LB.elem[j];

              j++;k++;

       while i<LA.last

              do LC.elem[k]=LA.elem[i];

              i++;k++;

       while j<= LB.last

              do LC.elem[k] = LB.elem[j];

              j++;k++;

       LC.last=LA.last+1+LB.last+1;

 

(未完待续)

Comments

平民的胜利

 北京时间稍早前,美国第四十四任当选总统巴拉克·侯赛因·奥巴马二世在华盛顿国会广场正式宣誓就职,并现场发表了就职演说。

 为观看现场直播,我前一天就在寻找这方面的信息,后来cnn上说是有视频直播,于是就赶忙去看,结果发现一个简洁的Ria下载就用了十多分钟的时间…不得已求助很久没有接触的电视,终于发现唯一的一个华语电视台要进行2个半小时的全程直播,于是强迫自己在电视机前坐了近3个小时。

 之所以关注这次的美国总统就职典礼,倒不是为了探究未来中美关系的走向,也并非羡慕这个民主、自由国家对自己所负历史责任的又一见证。08年的选战我多少还是通过网络了解一些,但并没有特别注重去思考一些问题,对新任总统对自己国民承诺的未来也并不很感兴趣。唯一吸引我的,是这位美国历史上首位黑人总统,“首位”的含义却不仅限于“黑人”,在很多方面,美国人民能摆脱长期以来精英政治思想的耳濡目染,对传统的政治家族、政治集团说“不”,推举出一位其父曾经是赴美留学生的非裔黑人作为即将领导他们的总统,这又何尝不是一次伟大的变革呢?就像美国人民,他们本就给予奥巴马极高的变革的期望,殊不知他们已经在主导一场变革,并再次行使了自己合法的权力。

 也有人对此次变革未来的前途并不持乐观态度,因为美国似乎还没有到那种甚至要改变“精英政治”思想的地步。4年后,甚至2年以后,或许人们会重新审视自己当初的判断与选择。但至少,变革所需要的重要一步已经迈出,无论如何是不会回到过去的状态了。

 至于新总统的施政方针和外交政策,毕竟还是有那么多的“精英们”去出谋划策,就轮不上外国人去指手画脚了。

Comments

ACM-ICPC及连载内容简介

有关ACM-ICPC赛事本身的知识请直接前往wiki,在此就不做额外说明了。

对于信息科学,国内乃至国际知名的竞赛大概有两个。除了ACM-ICPC,另一个是IOI国际信息学奥林匹克,后者主要面向中学生举办,内容上也比ACM层次稍浅。在中国,计算机普及教育从上世纪70年代末至80年代初开始,逐渐由面向少数科研专家扩展到基本覆盖全国中小学校,信息学竞赛的受关注度也逐渐增加,包括全国青少年信息学奥林匹克竞赛NOI、全国青少年信息学奥林匹克联赛NOIP以及ACM亚洲区域预赛等众多大型赛事的影响力也在稳步提升。

关于组织信息学竞赛的目的,固然是要继续推进计算机教育的深入普及,在一些重要赛事尤其是像ACM总决赛这样的顶尖赛事中,寻找和发现未来可能的顶级计算机专家也同样受人瞩目。

但是,从近年来尤其是近十年以来各类赛事的发展道路来看,由于创新含量匮乏,再加上诸如ACM等拥有长达30多年历史的老牌赛事很难推陈出新,是否能真正培养出未来的科学家也越来越令人担忧。目前比较普遍的一种情况是,针对竞赛本身的培训愈加严谨,选手水平每届都有较大提高,但通过竞赛能提供给人的信息却未能产生适应时代的效果。

尽管如此,无论如何学习ACM都是当前有志于深入计算机程序设计、算法分析与设计的同学一条明智且正确的选择。通常的建议是,如果你的数学和英语基础较好,那么尽量在刚进入大学时就直接接触ACM,如果相反的话,最好是先花时间奠定基础,怀揣深入学习的目的体验ACM的乐趣,尽管竞赛的意义对你来说已经变得并不重要了(部分“神童”可以忽略,但事实证明不断积累总不是一件坏事)。

 我们收集和连载有关ACM竞赛以及算法设计方面知识的主要目的是,通过共享目前应用广泛的算法设计方法,及其普遍原理,深入大家对程序设计内涵的理解,从而方便今后学习更加高端的信息学知识。

 连载的内容主要分为以下几个部分,常用数据结构分析,常用算法分析,ACM输入输出格式,高级设计和分析技术,高级数据结构分析,组合博弈算法等,在实际应用过程中,我们可能将根据实际情况进行一些内容调整。需要提前说明,本站提供的算法思想或伪代码描述仅供自学和查阅使用,并不涉及任何侵权问题,如有异议,请立即与我们取得联系。

Comments