2010年408数据结构算法题:循环左移

题目:

        设将n(n>1)个整数存放到一维数组R中。试设计一个在时间和空间两方面都尽可能高效的算法,将R中保存的序列循环左移p(0<p<n)个位置,即将R中的数据序列由(X_{0},X_{1},...,X_{n-1}),转变为(X_{p},X_{p+1},...,X_{n-1},X_{0},X_{1},...,X_{p-1})。要求:

(1)给出算法的基本设计思想

(2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释

(3)说明你设计的算法的时间复杂度和空间复杂度

解决方案1

(1)给出算法的基本设计思想

        利用多次reverse,第一次反转将

(X_{0},X_{1},...,X_{p-1},X_{p},X_{p+1},...,X_{n-1})

原地反转变为

(X_{n-1} ...,X_{p+1},X_{p},X_{p-1},...,X_{1},X_{0})

然后再将

 (X_{n-1} ...,X_{p+1},X_{p})与( X_{p-1},...,X_{1},X_{0})

分别原地反转,得到

(X_{p},X_{p+1},...,X_{n-1})(X_{0},X_{1},...,X_{p-1})

(2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释

void Reverse(int R[], int n){
    int i=0, j=n-1, temp;
    while(i<j){
        temp = R[i];
        R[i] = R[j];
        R[j] = temp;
        i++;
        j--;
    }
}

void leftShift(int R[], int n, int p){
    Reverse(R,n);
    Reverse(R,n-p);
    Reverse(&R[n-p],p);
}

(3)说明你设计的算法的时间复杂度和空间复杂度

         时间复杂度为O(n),空间复杂度为O(1)

解决方案2

(1)给出算法的基本设计思想

         借助长度为n的辅助空间tmp,先将R中前p个元素按原顺序移动到tmp中的后p个位置,然后将R中后n-p个元素直接按顺序移动到tmp中的前n-p个位置,最后将tmp中的所有元素按顺序移动到R中

(2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释

void leftShift(int R[], int n, int p){
    int tmp[n];
    for(int i=0;i<n;i++){
        tmp[i] = R[(i+p)%n];
    }
    for(int i=0;i<n;i++){
        R[i] = tmp[i];
    }
}

(3)说明你设计的算法的时间复杂度和空间复杂度

         时间复杂度为O(n),空间复杂度为O(n)

解决方案3

(1)给出算法的基本设计思想

        借助长度为p的辅助空间tmp,先将R中前p个元素按原顺序移动到tmp中,然后将后n-p个元素直接按顺序原地往左移动p个距离,最后将tmp中的p个元素按顺序移动到R中后p个元素的位置

(2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释

void leftShift(int R[], int n, int p){
    int tmp[p];
    for(int i=0;i<n;i++){
        if(i<p){
            tmp[i] = R[i];
        }
        else if( i > n-p){
            R[i] = tmp[i-n+p];
        }
        else {
            R[i-p] = R[i];
        }
    }
}

(3)说明你设计的算法的时间复杂度和空间复杂度

         时间复杂度为O(n),空间复杂度为O(p)

解决方案4

(1)给出算法的基本设计思想

        所有元素循环左移p个位置,意味着每个元素都要向左循环移动p个距离,每个元素的旧位置和新位置都是确定的,而且互相之间不会冲突。下标为i(0\leq i< n)的位置R[i] 的新数据将是原来位于R[i+p]的元素。那么可不可以一步到位,将R[i+p]的元素填入R[i] 呢?这样做有个前提,就是要把R[i] 原来的数据保存到别的地方,空出了位置R[i]才可以将新元素R[i+p]填入。同时R[i+p]的元素被挪走以后,R[i+p]就会空出位置,继续往后面寻找这个位置的新主人,这是一个迭代过程。因为每个位置的旧元素与新元素是确定的,所以不会出现多个元素竞争一个位置,不会出现类似于哈希表冲突一样情况。同时,由于元素总数n是确定的,所以我们只需要确保每个位置上的元素都是新元素,那么循环左移的任务就算完成了。

        上述的过程可以简述为:不断的一步到位将元素移动p个位置,然后让出原有位置,继续向后寻找这个空出来的位置对应的新元素。能够参与到这个过程的元素,形成了前后相互勾连的关系,我们将具有这个关系的所有元素定义为同一个分组,那么上述这个过程就可以看作是组内元素循环左移1个逻辑单位。

         这样大跨度寻找元素的过程,很类似于希尔排序。距离为p的多个元素将会组成一个小组,组内采用循环左移一个单位的算法实现移动。

        以p为步长的时候,会形成p个小组,第k(0\leq k<p)个小组的首元素便是R[k]

        当np的整数倍的时候,这p个小组将会相互独立,组内最后一个元素下标+p Mod n,得到的便是同一个小组的首元素的下标。

        假设n=M*p,M\in 实数,我们可以将这个n个元素按行优先存储到Mp列的数组中,如下图所示。

可以清楚的看到同一列的元素下标+p Mod n后得到的下标指向的元素是同一列的元素,最后一行的元素下标+p Mod n后将会回到同一列第0行。这个时候每一列的元素将会形成一个小组,总共p个小组,同一列的元素直接循环上移就可以完成将整个数组R循环左移p个位置的任务。这个时候p个小组是相互独立的,没有哪个组的元素下标+p Mod n后会进入别的组。

        当n不是p的整数倍的时候, 设 n Mod p=q,0\le q < p,这个时候p个小组将会不再独立,某个小组内最后一个元素下标+p Mod n,将会得到另外一个小组的首元素下标,如下图所示。

 将n个元素按每p个一行,排列到二维数组中,假设总共有M行,因为n不是p的整数倍,所以最后一行(图中蓝色单元格)的元素将不足p个。倒数第二行(图中红色单元格)后面p-q个元素 下标+p Mod n的结果将分别是 0,1,...,p-q-1,也就是说第q组的元素链接到了第0组,第q+1组的元素链接到了第1组,...,第p-1组的元素链接到了第p-q-1组 ,而倒数第一行(图中蓝色单元格)的元素  下标+p Mod n的结果将分别是p-q,p-q+1,...,p-1,也就是说第0组的元素链接到了第p-q组,第1组的元素连接到了第p-q+1组,...,第q-1组的元素链接到了第p-1组。

        所以本质上,第q组、第0组、第p-q组并不是独立的,而是属于同一条链的,首位相互链接。

        寻找哪些组属于同一个链,跟寻找哪些元素属于同一个组,本质上是相同的问题。前者取决于p是不是p-q的整数倍,后者取决于n是不是p的整数倍。前者的前进步长为p-q,后者的前进步长为p。

        一开始有p个组,分别属于p个链。当n是p的整数倍的时候,这个p个链没有相互勾连,是相互独立的,并且每个链都是循环链。当n不是p的整数倍的时候,发现原本的p个链中,有些链是首尾相接,链接到一起的,构成了更大的链,所以独立链的条数t也会减少。这些同属于一个链条的分组之间的步长从p缩短到q,可能还会进一步缩短。当发现链是循环链的时候,也就是元素下标+p Mod n回到了最初的位置的时候,就已经收敛了。当所有的链都收敛的时候, 独立链的条数t也稳定了下来,同属于一个链的分组之间的距离为t,步长为t,链与链的首元素(元素在R中的下标在0,1,...,t-1)是相邻的。

        于是当有多个独立的链存在的时候,每个链都是循环链,同一个链上的元素采用下标+p Mod n的方式是无法跳出当前链、到达别的链的,这个时候需要特殊检测是否循环到了最初的位置,如果是,要将最初的位置上的元素(提前存到了临时变量r中)填入最近生成的空位置,这个循环链的元素移动也就完成了。下一步要到另一个链进行步长为p的移动,于是采取从最初的元素右侧的一个元素开始(采取下标+1的方式进入新的独立链),直到移动元素的个数达到n为止。当t>1时,第0行中前t个元素X_{0},X_{1},...,X_{t-1}一定是分属于t个等价组,所以采取下标+1的措施进入另外的独立链是可行的。

        每个独立的链是循环链的证明:设m是n与p的最小公倍数,设m=s*n=t*p,m,s,n,t,p全都是整数。

        当n是p的整数倍的时候,n就是n与p的最小公倍数,s=1,t=M,R中的n个元素可以按行优先组成一个M行p列的二维数组,这个二维数组是完全填满的,一个不多一个不少,前M-1行中每一行的任意一个元素在R里的下标+p Mod n的结果就是同一列下一行的元素在R里的下标。最后一行的人一个元素在R中的下标+p Mod n的结果就是同一列第0行的元素在R中的下标。所以以步长p前进遍历过程中的所有访问到的元素全都是同一列的。

        当n不是p的整数倍的时候,将R中的n个元素按照行优先的顺序、每行p个元素,摆放成为二维数组,最后一行将会填不满p个单元,如果这个时候再来一个R继续填入,那么需要s个R才能恰好填满,因为m是n与p的最小公倍数,m一定可以被p整除,所以每一行都是满的,因为m是n的整数倍,所以填满这个p列的二维数组的,一定是s个R,一个元素不多,一个元素不少。前t-1行每个元素在R中的下标+p Mod n的结果就是同一列下一行的元素在R中的下标,最后一行的每个元素在R中的下标+p Mod n的结果就是同一列第0行的元素在R中的下标,所以构成一个循环链。但是由于这个t行p列的二维数组是由s个R的元素按先后顺序构成的,所以同一列的元素,将会来自于s个R,从上到下将会分为s个part,每个part来自于一个R,不同的part来自于不同的R。同一个part里的元素在R中的下标 Mod n的结果是相同的,但是不同的part中的元素在R中的下标 Mod n的结果必定不相同(因为m是n和p的最小公倍数,如果相同则m不是n与p的最小公倍数,n和p还有一个公倍数比m还小)。回想到一开始的时候,我们根据下标 Mod p 的结果将R中的n个不同的元素分为p个不同的小组,R中前p个元素便是这p个小组的首元素。所以在s个R构成的t行p列的二维数组中,同一列的s个part的元素,分属于不同的小组,但是属于一个循环链。

(2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释

void leftShift(int R[], int n, int p){
    int cur, next, r, start = 0;
    cur = start;
    r = R[cur];
    for(int i=0;i<n;i++){
        next = ( cur + p) % n;
        if(next == start){// there is a circle, return to the start
            R[cur] = r;
            start++;
            cur = start;
            r = R[cur];
        }
        else {
            R[cur] = R[next];
            cur = next;
        }
    }
}

(3)说明你设计的算法的时间复杂度和空间复杂度

         时间复杂度为O(n),空间复杂度为O(1)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值