关闭

C/C++面试之算法系列--约瑟夫环:每隔两个循环删除数组元素,求最后删除者的下标问题

标签: 面试算法liststruct优化null
6557人阅读 评论(3) 收藏 举报
分类:
 
××××××××××××××××××××××××××××××××
 
“约瑟夫环:每隔两个循环删除数组元素,求最后删除者的下标问题”
Sailor_forever sailing_9806@163.com 转载请注明
 
×××××××××××××××××××××××××××××××××
有一个数组a[1000]存放0--1000;要求每隔二个数删掉一个数,到末尾时循环至开头继续进行,求最后一个被删掉的数的原始下标位置(原来的数可能是无序的,另外数是否重复从题意无法确定)。
以8个数为例:
   {0,1,2,3,4,5,6,7} 0-->1-->2(删除)-->3-->4-->5(删除)-->6-->7-->0(删除),如此循环直到最后一个数被删除。
 
考点:
本题的关键是如何理解删除的概念,一种是将其彻底删除,将后面未删除的全部前移,这将浪费大量的时间用来移动后续数据;另外一种只是仍然存在但已经失去意义,遍历过程中不管之。
另外一个关键是如何在移动的过程中保持数据的原始下标,因为数据本身可能是无序的,下标和数据可能没有关系,并且还可能重复
 
性能分析:
时间效率:循环队列法由于每次扫描的数都是未删除的,时间整体效率最高;标志数组法只是将此数标记为删除了,但遍历时仍然要扫描;链表法要建立链,时间效率低
空间效率:标志数组法,未申请额外的空间,空间效率最高;循环队列法至少申请第一次未删除的空间,另外作为接口函数来实现的话,由于新申请的空间不能与传入的数组地址相连,因此有局限性;链表法额外建立链表,空间效率最低
 
约束因素:
对于只读数组,普通的标志法都不能用了,将高位置1遍历完后清除的方法借鉴意义最高;时间和空间效率最均衡;链表法可以处理只读数组的问题;循环队列法此时无法实现;当然对于标志法,可以额外申请空间保存标志,也可以处理只读问题,但空间效率下来了
 
面试中的最优选择:
将高位置1遍历完后清除,效率最高
数组方式二的优化版本,三层while,程序的逻辑功能划分明确
数组方式一的优化版本,程序的结构易懂清晰
循环队列法最难想到,个人认为相对于标志数组更有创新性
 
××××××××××××××××××××××××××××××××
方法1:访问原数组,置删除标志
这题目如果是面试题,那考的就是用数组,增加难度的
××××××××××××××××××××××××××××××××
数组方式一:
const   int   size=1000;    
void
   ArrayTest1 (void)  
  {  
        int   arr[size];  
        int   currentSize=size;   //
指示当前数组中还剩余有效元素的个数,为1时表示删除/完毕;  
        int   count=0;       //计数用;  
        for(int   k=0;k<size;k++)  
        {  
        arr[k]=k;  
        }    
          // i用%循环计数,终止条件是删除的只剩最后一个数了
        for(int   i=0;(i<size)   &&   (currentSize!=1);  
i=(i+1)%size)  
        {  
        if(arr[i]!= -1 )   //
1为已经删除的标志,未删除对之计数,已经删除的则看下一个
        {  
// 按照计数间隔计数,达到间隔时删除数据
       
if(count>=0   &&   count<2)  
        {  
        count++;  
        }else   if(   count==2)   //
逻辑有点乱
        {  
            arr[i]= -1;//将此元素做上标记,表示删除此时的元素;  
    currentSize--;//有效元素减一;  
    count=0;//并将计数值归零;  
        }  
        }      
        }  
        for(int   j=0;j<size;j++)   // 浪费时间啊

        {  
        if(arr[j]!=-1)  
        {  
        cout<<"the   result   is   :"<<j<<endl;  
        break;  
        }  
        }  
  }
 
优化版本:宏定义意义明确,更改方便,是良好的编程习惯,要在笔试面试中展现这种特点
删除数据时保存了其位置和实际的数据值,无需最后一次扫描
三层判断条件功能清晰:总数,是否删除,是否达到间隔值
 
#define   SIZE 1000
#define   STEP 2
#define   DELFLAG (SIZE + 1) 
void   ArrayTest1Opt(void)      
{  
       int   arr[SIZE];  
       int   currentSize=SIZE;   //指示当前数组中还剩余有效元素的个数,为0时表示删除/完毕;  
       int   count=0;       //计数用;
       int   i = 0;
       int     lastdelindex = 0;         // 用来保存每次删除值的位置,不用留最后一个然后遍历
int     lastdelvalue = 0;         // 用来保存每次删除值,不用留最后一个然后遍历
 
       for(int   k=0;k<SIZE;k++)  
       {  
              arr[k]=k;  
       }   
       // i用%循环计数,终止条件是删除所有的数了
       //for(int   i=0; currentSize!=0; i=(i+1)%SIZE)
       while(currentSize!=0) 
       {  
              if(arr[i]!= DELFLAG )   // DELFLAG为已经删除的标志,未删除对之计数,已经/删除的则看下一个
              {  
                     // 按照计数间隔计数,达到间隔时删除数据
                     if( count++ ==STEP)   //注意++的位置
                     {  
                            lastdelindex = i;      // 用来保存每次删除值的位置
lastdelvalue = arr[i];       // 用来保存每次删除值的位置
                            arr[i]= DELFLAG;//将此元素做上标记,表示删除此时的元素;  
                            currentSize--;//有效元素减一;  
                            count=0;//并将计数值归零;  
                     }  
              }
              i=(i+1)%SIZE;      
       }  
      
       cout<<"the original array location of the last del value is: "<<lastdel<<endl;  
}
 
××××××××××××××××××××××××××××××××
数组方式二:两重while循环,将间隔计数的直接写成了程序,未用循环,这样移植性差了
 
//#define   SIZE 1000
//#define   STEP 2
//#define   DELFLAG (SIZE + 1)
 
void ArrayTest2(void)
{
int arr[SIZE];
for (int i=0;i<SIZE;++i)
arr[i]=i; // 实际情况并非一定如此啊
int j=0;
int count=0;
while(count<SIZE-1)     // 999保留了最后一个未删除的数
{
while(arr[j%1000]==DELFLAG)
j=(++j)%SIZE;
 
j=(++j)%SIZE;       // 第一个未访问的数
 
while(arr[j%SIZE]==DELFLAG)
j=(++j)%SIZE;
 
j=(++j)%SIZE;       // 第二个未访问的数
 
while(arr[j%SIZE]==DELFLAG)
j=(++j)%SIZE;
 
arr[j]=DELFLAG;          //删除第三个未访问的数
++count;
}
while(arr[j]==DELFLAG)      // 扫描最后一个未删除的数
j=(++j)%SIZE;
 
cout<<j<<endl;
}
 
优化版本:程序的逻辑功能划分明确,每个while处理一层逻辑,结构更易懂,程序的移植性强,while中的循环次数可随意更改
上面方法中的step在程序中固定了
void ArrayTest3Opt(void)
{
int arr[SIZE];
for (int i=0;i<SIZE;++i)
arr[i]=i; // 实际情况并非一定如此啊
int j=0;
int count=0;
int stepcounter=0;
int delindex = 0;
while(count<SIZE) // 删除总次数
{
// 删除一个
while(stepcounter <= STEP) // 寻找第三个未访问的数
{
while(arr[j%SIZE]==DELFLAG) //剔除已删除的
j=(++j)%SIZE;
 
j=(++j)%SIZE;       // (++j)中的j为一个未访问的数,而非保存后的j故下面delindex = j -1;
stepcounter ++;
}
//delindex = j -1;     // 由于上面j加一后可能溢出重头开始了,因此先加SIZE
delindex = (j + SIZE -1)%SIZE;    // 保存当前删除数的下标
stepcounter = 0;
arr[delindex]=DELFLAG;             // 删除第三个未访问的数
++count;
}
 
××××××××××××××××××××××××××××××××
标志数组方式三:
对于只读数组,也可以置标志,访问完毕后能够恢复初始序列即可
当前存放的数是正数,且有一定的范围,可是利用其最高位做为已经删除标志,最后一个删除的数其最高位为0,下标可知,实际数据可知。所以数据删除完后,清除最高位的删除标志,即可恢复原始序列。
#define   delFLAG (1 << ((sizeof(int)*8) - 1))
// 注意上述首位置1的定义,考虑了int字节个数的影响,对于嵌入式软件工程师来说时刻考虑跨平台的移植性能,是个很重要的素质 
void   _ArrayTest3(void)  
{  
       int   arr[SIZE];  
       int   currentSize=SIZE;   //指示当前数组中还剩余有效元素的个数,为0时表示删除/完毕;  
       int   count=0;       //计数用;
       int     lastdel = 0;
       int   i = 0;
 
       for(int   k=0;k<SIZE;k++)  
       {  
              arr[k]= SIZE - k;  
       }   
       // i用%循环计数,终止条件是删除的只剩最后一个数了
       while(currentSize!=0)
       {  
              if(arr[i] >= 0 )   // 当前数大于等于0时,未删除,已经删除的则看下一个
              {  
                     // 按照计数间隔计数,达到间隔时删除数据
                     if( count++ == STEP)  
                     {  
                            lastdel = i;
                            arr[i] |= delFLAG; //将此数最高位置为1,此时其为负数,表示删除  
                            currentSize--;//有效元素减一;  
                            count=0;//并将计数值归零;  
                     }  
              } 
              i=(i+1)%SIZE;
       }  
       cout<<"the original array location of the last del value is: "<<lastdel<<endl;
      
       // 清除最高位的1,恢复原始数组
       for(k=0;k<SIZE;k++)  
       {  
              arr[k] &= ~delFLAG; // 注意这种清除方式的移植性很高 
       }
      
}
 
时间复杂度比在初始数组中置标志多了最后的清除删除标志,空间复杂度为0,没有额外申请空间保存访问标志和下标,相比额外申请1000个空间处理只读数组空间效率高。
比下面循环队列的空间效率高,时间效率低,因为此法访问了已经删除的无效数据。
 
这个题看似说只能使用数组,但可以考虑使用数组完成链表的功能,建议再开一个1000大小的数组,每个元素里面放的是另外一个数组中对应元素的脚标,即0-999.然后循环按此数组循环,修改链表是通过置访问标志来实现的,最后一个未置标志的数就是原始下标了,原数组中就是最后删除的实际数据。不用动原来的数组里面的数据..
 
××××××××××××××××××××××××××××××××
××××××××××××××××××××××××××××××××
 
方法2:循环队列,将未访问的数据移动到循环队列末尾   
  #define   MaxCount   1000    
  void   RoundQueue(void)  
  {  
  int   list[2*MaxCount];                               //多分配一倍的空间,用来放循环数据  
  int   i,index=0,tail,delcount=0;  
   
  for(i=0;i<MaxCount;i++)   list[i]=i;     //初始化,存放0~999  
   
  tail=MaxCount; //list[tail]用来存放不删除的数据  
   
  while(delcount<MaxCount) //删除1000个就退出  
  {  
   //第三个数据删除,此处有误,因为index循环时重新赋值了,导致((index %3)的错误 
 
if((index %3)==2)
  {  
  delcount++;  
  printf("%d/t",list[index]);  
  }  
  else       //
前两个数据放到队列末尾,  
  {  
  list[tail]=list[index];  
  tail=(tail+1) % (2*MaxCount);  
  }  
  index=(index+1) % (2*MaxCount);//步近 
  }  
  return   ;  
  }
// 缺点,不能输出最后一个删除数的原始下标,只能输出其值,因为该数组可能是随机的,下标与值可能不一致
// 此题的考点就在于删除过程中拷贝了数据后如何保存原始位置,否则的话我每次删除一个值后,将后面所有的数前移,然后再删,没有意义了 
 
××××××××××××××××××××××××××××××××
循环队列改正版
void   RoundQueue(void)
{  
int   list[2*MaxCount]; //多分配一倍的空间,用来放循环数据  
int   i,index=0,counter=0,tail,delcount=0; 
int    lastdel = 0;
 
for(i=0;i<MaxCount;i++)   list[i]=i;     //初始化,存放0~999  
 
tail=MaxCount; //list[tail]用来存放不删除的数据  
 
while(delcount<MaxCount) //删除1000个就退出  
{  
counter++;
if(counter==3) //第三个数据删除,单独计数,不用index  
{  
delcount++;
counter = 0;
lastdel = list[index]; 
//printf("%d/t",list[index]);  
}  
else       //前两个数据放到队列末尾,  
{  
list[tail]=list[index];  
tail=(tail+1)%(2*MaxCount);  
}  
index=(index+1)%(2*MaxCount);//步近  
}  
 
printf("%d/n",lastdel);
}
 
××××××××××××××××××××××××××××××××
循环队列优化版
 
//#define   STEP 2
#define   FST_ROUND_LEFT   (MaxCount - MaxCount/(STEP+1))
//#define   ARRAYSIZE (MaxCount+MaxCount)
#define   ARRAYSIZE (MaxCount+FST_ROUND_LEFT)
// 在第一轮里未删除的数,应该保存未删除数据的下标
// 以后待删除的数已经都是下标了,应该保存其值,最后删除的数就是其原始数的下标了
 
void   RoundQueueOpt(void)  
{  
       int   list[ARRAYSIZE];  //多分配一倍的空间,用来放循环数据,其实多分配//FST_ROUND_LEFT即可了,即 list[FST_ROUND_LEFT+MaxCount],后面的循环值要改下 
       int   i,index=0,counter=0,tail,delcount=0; 
       int           lastdel = 0, firstround = 1;
 
       for(i=0;i<MaxCount;i++)  
              list[i]=MaxCount - 1 - i;     //初始化,存放999~0,此时下标和数是非对应的  
 
       tail=MaxCount; //list[tail]用来存放不删除的数据下标或已经保存的下标  
 
       while(delcount<MaxCount) //删除1000个就退出  
       {  
              counter++;
              if(counter==3) //第三个数据删除  
              {  
                     delcount++;
                     counter = 0;
 
                     if(firstround)
                            lastdel=index;
                     else
                            lastdel=list[index];
                     //printf("%d/t",list[index]);  
              }  
              else       //前两个数据放到队列末尾,  
              {
 
                     if(firstround)
                            list[tail]=index;
                     else
                            list[tail]=list[index];
 
                     if(firstround)
                            if(tail - MaxCount == FST_ROUND_LEFT - 1)
                            {
                                   firstround = 0;       // 清除第一轮标志,以后保存值(保存后的下标)
                                   printf("%d/n",FST_ROUND_LEFT);
                            }
 
                     tail=(tail+1)%(ARRAYSIZE);  
              }  
                    
              index=(index+1)%(ARRAYSIZE);//步近  
       }  
 
       printf("%d/n",lastdel);
}

把数组当作一个循环队列,就可以循环运算了,关键在于将数据后移的过程中要保存住其初始下标位置,是通过计第一次循环来实现的  

每次扫描的数都是有效的,因此此法相比标志数组法效率更高。但申请了额外的数组,因此空间效率较标志数组法低。
 
 
××××××××××××××××××××××××××××××××
××××××××××××××××××××××××××××××××
方法三:链表
如果先用链表存储数组的话,将数据和下标封装起来,建立链表,利用指针即可达到快速移动数据的目的;将原数组封装成单向循环链表,每隔两个删除一个节点,最后一个删除的节点即是所求,其保存了原始数据及其下标
当然由于此为单独建立了链表,原有的数组数据并未更改,因此得到了最后一个删除数据的下标即可访问原数组得到对应的数据;因此数据成员可以不含data
此题是考链表的一个典型的例子
 
struct node
{
int data;
int index;
node* next;
};
#define null 0
 
int LinkTest2(void)
{
int arr[SIZE];
for (int i=0;i<SIZE;++i)
arr[i]=SIZE-i; // 实际情况可能是随机的
 
node* head=new node;
head->data=arr[0];
head->index=0;
head->next=null;
node* p=head;
for(i=1;i<1000;i++)
{
node* tmp=new node;
tmp->data=arr[i];
tmp->index=i;
tmp->next=null;
head->next=tmp;
head=head->next;
}
head->next=p;
while(p!=p->next) // p指向自己时,循环链表只剩一个元素了
{
p->next->next=p->next->next->next;
p=p->next->next;
}
// 存在内存泄漏
 
cout<<p->index<< ' '<< p->data<<endl;
return 0;
}
 
××××××××××××××××××××××××××××××××
typedef   struct   Lnode  
  {  
          int   num;  
          struct   Lnode   *next;  
  }Lnode,*Linklist;  
   
  Lnode   *Create()  
  {  
          Lnode   *head,*p1,*p2;  
          int   i;  
          head=(Linklist)malloc(sizeof(Lnode));  
         
head->next=NULL;  
          head->num=0;                     /*
头节点赋初值*/  
          p2=head;  
   
          for(i=1;i<1000;i++)  
          {  
                  p1=(Linklist)malloc(sizeof(Lnode));  
                  p1->num=i;  
                  p1->next=NULL;  
                  p2->next=p1;  
                  p2=p1;  
          }  
          p1->next=head;   // 单向循环链表
   
          return   head;  
  }  
   
  void   Del(Lnode   *head)  
  {  
          Lnode   *n,*m,*q;  
          int   i=0;  
          m=head;  
   
         // m first n second q third should be deled
          while(i<1000)   //
未删完,当剩下三个以内时,释放q删除后可能出问题

          {  
                  n=m->next;  
                  q=n->next;             /*   the   elem   which   will   be   deleted   */  
                  n->next=q->next;              
                  free(q);   // 不释放q没有问题,但存在内存泄漏啊

                  m=n->next;  
                  i++;  
          }  
         
printf("%d/n",m->num);  
  }  
   
 void   LinkTest1(void)  
{  
      Lnode   *q;  
 
      q=Create();  
      Del(q);  
}
 
××××××××××××××××××××××××××××××××
 
一点思考:
这个题目在计算机界叫约瑟夫环,有很多模型是根据此建立的
 
有没有听说过一个游戏,叫出局,这个是游戏的抽象简化,游戏如下:一群人(n个)围成一圈,选中某人作为开始从1数数,数到k的人从圈中出来,下一位从1开始轮流数,看最后剩下谁.(k=1,2...n-1),就是%符的应用而已

参考鸣谢:http://topic.csdn.net/t/20060227/11/4579928.html
 
0
0

猜你在找
【直播】机器学习&数据挖掘7周实训--韦玮
【套餐】系统集成项目管理工程师顺利通关--徐朋
【直播】3小时掌握Docker最佳实战-徐西宁
【套餐】机器学习系列套餐(算法+实战)--唐宇迪
【直播】计算机视觉原理及实战--屈教授
【套餐】微信订阅号+服务号Java版 v2.0--翟东平
【直播】机器学习之矩阵--黄博士
【套餐】微信订阅号+服务号Java版 v2.0--翟东平
【直播】机器学习之凸优化--马博士
【套餐】Javascript 设计模式实战--曾亮
查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:838889次
    • 积分:11101
    • 等级:
    • 排名:第1413名
    • 原创:231篇
    • 转载:15篇
    • 译文:0篇
    • 评论:443条