【编程珠玑】【第二章】问题B

问题B:将一个n元一维向量向左旋转i个位置。例如,当n = 8且i = 3时,向量abcdefgh旋转为defghabc。

方法一、使用一个字节的额外空间开销。

采用每次向左移一位的方法,循环i次。当然也可以使用向右移动的方法,循环length - i次。以向左移动为例,共需要移动i趟,首先把str[0]赋值给临时变量temp,剩余的字符向左移动一位,即str[k]=str[k+1],移动完成后把临时变量temp赋值给str[n-1]。

该方法比较笨,但是也是最容易想到的,它空间开销小,但是时间开销非常大,时间复杂度为O(n^2)。这是因为两层的嵌套循环,效率太低。

#include <stdio.h>
#include <string.h>
#include <assert.h>
void RightShift(char *str, int k){
    if(str == NULL){
        return;
    }
    int length = strlen(str);
    k = k % length;
    int i = 0;
    //虽然传入的参数是k,但这是左移的次数,实际需要右移length-k个位置
    int tmp = length - k;
    while(tmp--){
        char temp = str[length - 1];
        for(i = length - 1; i > 0; i--){
            str[i] = str[i - 1];
        }
        str[0] = temp;
    }
}
void LeftShift(char *str, int k){
    if(str == NULL){
        return;
    }
    printf("string :%s\n",str);
    int length = strlen(str);
    k = k % length;
    int i = 0;
    while(k--){
        char temp = str[0];
        for(i = 0; i <length-1 ;i++){
            str[i] = str[i + 1];
        }
        str[length-1] = temp;
    }
}
int main(void){
    char a[]="abcdefgh123"; //使用char *a="abcdefgh123";异常退出,貌似不能赋值。
    LeftShift(a,3);
    printf("string :%s\n",a);
    return 0;
}

方法二、使用n个字节的额外空间开销

减小时间开销的一个基本思想是以空间换时间。这个算法使用一个新的长度为n的字符数组temp保存原始字符串的副本,然后利用temp对原始字符串的每个元素重新赋值成新的、旋转后的字符串。

此算法也相对比较简单,稍显巧妙的是str[j] = temp[(j+k)%length];给原数组重新赋值的操作。但是此算法会开辟O(n)的空间,以加速程序执行。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void LeftShift(char *str, int k){
    int length = strlen(str);
    char * temp = (char *)malloc(length*sizeof(char));
    int j;
    for (j=0;j<length;j++){
        temp[j] = str[j];
    }
    for (j=0;j<length;j++){
        str[j] = temp[(j+k)%length];
    }
}

方法三、使用i个字节的额外空间开销。

显而易见,上述两种算法远非最佳算法,有在时空上取得双赢的改进的可能。第三种算法将字符串的前i个元素复制到一个临时字符数组temp中,将原始字符串余下的n-i个元素左移i个位置,最后将最初的i个元素从temp中复制到余下的位置。这样就实现了移动。

这种算法看上去和第二种没太大差别,但无论从时间开销还是空间开销上来讲,都要比第二种好。原因在于虽然原始字符串中的每个位置都要发生变化,但没有必要花费n个字节的内存开销保存原始字符串的完整副本,只需保存前i个位置的元素。

可是此算法依然比较笨笨的,而且改进的效果不稳定,它使用了i个额外的位置仍然比较浪费空间,所以并不是十分好。

void LeftShift(char *str, int k){
    char * temp = (char *)malloc(k*sizeof(char));
    int length = strlen(str);
    int j;
    for (j=0;j<k;j++){
        temp[j] = str[j];
    }
    for (j=k;j<length;j++){
        str[j-k] = str[j];
    }
    for (j=length-k;j<length;j++){
        str[j] = temp[j-length+k];
    }
}

方法四、“翻手”算法,也叫“求逆”算法

来看一个有趣的实现字符串循环左移的算法。在具体讲这种算法之前,先来看看线性代数里的转置。(AB)T等于什么?等于BTAT。那么(ATBTT等于什么?等于(BTT(ATT,即BA。啊哈!我们用三个步骤就可以完成这个字符串的循环左移了。对于字符串来讲,转置在这里就是逆置。把原始字符串分成ab两部分,a是前i个元素,b是后n-i个元素,首先对a求逆,得到a-1b,然后对b求逆得到a-1b-1,然后对整体求逆得到(a-1b-1-1=ba。 8个字符的字符串abcdefgh -> defghabc需要左移三个元素(或者右移5个元素),使用三次翻转的基本思路为:

reverse(0,i-1); //cba defgh——左边i个元素翻转
reverse(i,n-1); //cba hgfed——右边n-i个元素翻转
reverse(0,n-1); //defghabc——整体翻转,共三次翻转,时间复杂度为O(n)。

void Swap(char *a, char *b){
    char temp = *a;
    *a = *b;
    *b = temp;
}
void Reverse(char *str, int left, int right){
    if(str == NULL || left >= right){    //assert((str != NULL)&&(left <= right));
        return;
    }
    while(left < right){
        Swap(&str[left], &str[right]);
        left++;
        right--;
    }
}
/*等价的Reverse可写作如下:
void Reverse(char* str,int left, int right) {
    if(str == NULL || left >= right){    //assert((str != NULL)&&(left <= right));
        return;
    }
    int mid = (left + right)/2,i,j;
    for ( i = left,j = right;i <= mid;i++,j--)  {
        Swap(&str[i], &str[j]);
    }
}*/
void LefttShift(char *str, int k){
    if(str == NULL){
        return;
    }
    int length = strlen(str);
    k = k % length;

    Reverse(str, 0, k - 1);
    Reverse(str, k , length - 1);
    Reverse(str, 0 , length - 1);
}

方法五、Juggling act,杂耍算法。

为了满足O(1)空间的限制,延续方法一的思路,如果每次直接把原向量的一个元素移动到目标向量中它的应该出现新位置上就行了。先把array[0]保存起来,然后把array[i]移动到array[0]上,array[2i]移到array[i]上,直至返回取原先的array[0]。但这需要解决的问题是,如何保证所有元素都被移动过了?数学上的结论是,依次以array[0],...,array[gcd(i,n)-1]为首元进行循环即可,其中gcd(a,b)是a与b的最大公约数。

正如“杂技”一词所暗示的一样,这个算法就像在玩杂耍球,你要让它们中的每一个都在合适的位置上,这些球,除了手中有一个,其它几个都在空中。如果不熟悉,很容易手忙脚乱,把球掉的满地都是。

先从几个概念开始:

同余:如果两个整数a,b除以同一个整数m得到余数相同,则称a,b对于模m同余。记作a ≡ b (mod m)

数学描述:设m不等于0, 若m|(a-b)即a-b=km,则称m为模,a同余于b(模m),以及b是a对模m的剩余。记作 a≡b(mod m)。

同余类:所谓同余类是指以某一特定的整数为模,按照同余的方式对全体整数进行分类。对给定的模m,有且恰有m个不同的模m同余类。它们是:0 mod m,1 mod m,…,(m-1)mod m。

完全剩余类:由上可知,所有的整数以m为模可以划分为m个没有交集的集合。分别从每个集合中取一个整数组成一个集合,则该集合中的m个整数互不同余(除以m的余数互不相同),这个集合就叫做完全剩余类。

基于以上知识,我们可以证明这样一个事实,即如果i和n互质的话,那么序列:0, i mod n , 2i mod n , 3i mod n , …… , (n-1)*i mod n,就包括了集合{0,1,2,……n-1}的所有元素,下一个元素(n)*i mod n 又是0。我们为什么会有这样的结论呢,下面来证明一下:

前提条件: 对于模n来说,序列0,1,2,……,n-1本身就是一个完全剩余类,即他们之间两两互不模n同余。

证明步骤:

1)从此序列中任取两个数字xi,xj(0 <= i,j <= n-1),则有Xi≠Xj (mod n),

注:这里由于不能打出不同余字符因此用不等于替代

2)由于i和n是互质的,对于序列中任意两个数字xi,xj,有xi * i ≠ xj * i(mod n),这就说明xi从0开始一直取值到n-1,得到的序列0 * i,1 * i,2 *i,……(n-1)*n是一个完全剩余类,即集合{0,1,2,……n-1}。

概念介绍结束,有了这些结论之后,如果i和n互质,下面的赋值过程便能完成所有位置的值的移动:

    t = X[0]
    X[0] = X[i mod n]
    X[i mod n] = X[2i mod n]
    …….
    X[(n-2)*i mod n] = X[(n-1)*i mod n]
    X[ (n-1)*i mod n] = t

以上赋值操作符的两边都得到了一个完全剩余类,也就是说所有的0 ~ n-1的所有位置都被移动过了,每次赋值将一个元素的放置到了最终位置上,可见由于i,2i,……之间的偏移量是相同的,所以整个操作实际上就是讲序列向左移动i个位置(超过了开始位置的部分会被连接到最右边去)。

算法正确执行的前提是i是与n互质的,这样通过循环的每隔i元素并对n取余的遍历方式能够不重复的访问到数组a[n]中的每个元素,并通过一步赋值将其移动到正确的位置上,所需要的额外空间仅仅用于保存被第一个赋值操作所覆盖掉的数字,待全部的n-1个位置移动完毕后,将这个额外空间所保存的数赋值给第n个位置。

根据以上我们直到如果i和n互质,我们可以一轮循环完成左移任务。那么如果i和n不是互质的呢?那需要利用同余的结论,让i和n互质,构造一对互质的数i’和n’,其中i’= i/gcd(i,n)和n’= n/gcd(i,n)。这意味着每g=gcd(i,n)个元素组成块,整个数组共有n/gcd(i,n)个块,这样每趟循环只能针对每组中的一个位置的元素,把所有的元素处理完毕需要进行g轮循环

//*把字符串循环左移k位*/
void LeftRotateString(char* str,int k)  {
    assert(str != NULL && k > 0);
    int length = strlen(str);
    int gcdNum = gcd(length,k);     //每组包含元素数目g=gcd(n,k)
    for (int i = 0;i < gcdNum;i++)  {
        char temp = str[i];            //每组的起始位置,注意不能写成0
        int first = i;
        int next = (first + k) % strLen;
        while(next != i)  {
            str[first] = str[next];
            first = next;
            next = (first + k) % strLen;
        }
        str[first] = temp;        //临时变量中存储每一趟的循环的最后一个字符
    }
}  

注意:对于左旋转字符串,我们知道每个单元都需要且只需要赋值一次,什么样的序列能保证每个单元都只赋值一次呢?

A. 对于正整数k、n互为质数的情况,例如k = 3, n =4的0,1,2,3情况,算法执行如下:

tmp = str[0],          //把第一个元素保存起来
str[0] = str[3],       //i==0, i*k=0,(i+1)*k%n==0*3%4==3
str[3] = str[2],       //i==1,i*k=3, (i+1)*k%n==2*3%4==2
str[2] = str[1],       //...
str[1] = tmp,          //放置第一个元素

  B. 对于正整数k、n不是互为质数的情况(因为不可能所有的k、n都是互质整数对),那么我们把它分成一个个互不影响的循环链,所有序号为(j + i * k)%n(j为0到gcd(n,k) - 1)之间的某一个整数,i = 0:n-1)会构成一个循环链,一共有gcd(n,k)个循环链,对每个循环链分布进行一次内循环就可以了。

仍然不是很懂,有机会在琢磨琢磨。编程珠玑里面提到了Gries and Mills的一篇总结报告,<swap section>。该报告中提到了三种算法,其中之一就是本算法,不过它称之为Dolphine swap算法,Dolphine swap的基本思路是,把x[0]先保存起来,然后把x[i]放到x[0],把x[2i]放到x[i]...如果i和n互质的话,一个循环就能将所有的字符串都放好,最后把t填充到最后一个空位中。如果i和n不互质的话,则需要做gcd个循环。

void dolphine( char* s, int pos ){
    int n=strlen(s);
    int r = gcd( n, pos );
    int i=0;
    for( i=0; i<r; i++ ){
        char t=s[i];
        int j=i;
        while(1){
            int k = j+pos;
            if( k >= n )
                k -= n;
            if( k == i )
                break;
            s[j] = s[k];
            j =k ;
        }
        s[j] = t;
    }
}
//顺便给出gcd的算法
int gcd( int a, int b ){
    while( a != b ){
        if( a>b )
            a -= b;
        else
            b -= a;
    }
    return a;
}

方法六、分段递归交换算法。

书上介绍:旋转向量x其实就是交换向量ab的两段,得到ba(a代表x中的前i个元素)。假设a比b短,将b分为b1和b2两段,使b2有跟a相同的长度,然后交换a和b2,也就是ab1b2交换得到b2b1a,a的位置已经是最终的位置,现在的问题集中到交换b2b1这两段,又回到了原来的问题。不断递归下去,到b1和b2的长度长度相等交换即可。

书中说需要用递归解之,单个人感觉并无必要用递归,两次交换数据块即可。

//交换操作,如下所示
//swap x[a .. a+offset-1] and x[b .. b+offset-1]
void swap(int array[], int a, int b, int offset){
    int temp;
    for (int i = 0; i < offset; i++) {
        temp = array[a + i];
        array[a + i] = array[b + i];
        array[b + i] = temp;
    }
}
//交换主要代码
void swapShift(int *array, int n, int rotdist){
    int p = rotdist;
    int i = p;
    int j = n - p;
    while (i != j)  {
        if (i > j)  {
            swap(array, p - i, p, j);
            i -=j;
        }  else  {
            swap(array, p - i, p + j - i, i);
            j -= i;
        }
    }
    swap(array, p - i, p, i);
}

求逆算法扩展

求逆算法通过使用void Reverse(char* str,int left, int right)转置函数函数,能够对任意的字符串向量求逆。常见于面试题目中,如:(google面试题)用线性时间和常数附加空间将一篇文章的所有单词倒序。举个例子:This is a paragraph for test 处理后: test for paragraph a is This

如果使用求逆的方式,先把全文整体求逆,再根据空格对每个单词内部求逆,是不是很简单?另外淘宝今年的实习生笔试有道题是类似的,处理的对象规模比这个扩展中的“一篇文章”小不少,当然解法是基本一样的,只不过分隔符不是空格而已,这里就不重述了。

原文地址:https://www.cnblogs.com/blastbao/p/8306600.html

时间: 2024-10-06 02:38:54

【编程珠玑】【第二章】问题B的相关文章

一维向量旋转算法 编程珠玑 第二章

看了编程珠玑第二章,这里面讲了三道题目,这里说一下第二题,一维向量旋转算法. 题目:将一个n元一维向量(例数组)向左旋转i个位置. 解决方法:书上讲解了5种方法,自己只想起来2种最简单方法(下面讲的前两种). 1.原始方法. 从左向右依次移动一位,对所有数据平移:这样循环i次,算法最坏时间复杂度达n^2.耗时不推荐. 2.空间换时间. 顾名思义,申请一个i长度的空间,把前i半部分放到申请空间中,再把后面的所有数据向左移动i个位置,最后把申请的空间中的数据放到后半部分.浪费空间,不推荐. 3.杂技

编程珠玑第二章

编程珠玑第二章 A题 给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中一32位整数. 1.在文件中至少存在这样一个数? 2.如果有足够的内存,如何处理? 3.如果内存不足,仅可以用文件来进行处理,如何处理? 答案: 1.32位整数,包括-2146473648~~2146473647,约42亿个整数,而文件中只有40亿个,必然有整数少了. 2.如果采用位数思想来存放,则32位整数最多需要占用43亿个位.约512MB的内存空间. 可以采用前一章的位处理方法.然后判断每个in

集体智慧编程_第二章(提供推荐)_1

前言:最近正在拜读Toby Segaran先生写的集体智慧编程,首先感谢Toby Segaran先生将知识以书本的方式传播给大家,同时也感谢莫映和王开福先生对此书的翻译,谢谢各位的不辞辛苦.首先在写随笔之前,跟各位分享一下我的编程环境:win7系统,python版本是2.7.10,开发环境我选择的是pycharm程序.本书的第一章为集体智慧导言,主要介绍的何为集体智慧和机器学习的相关概念和其局限性,以及与机器学习相关的例子和应用场景.下面开始机器学习第二章--提供推荐的相关内容. 本章主要内容:

java编程思想 第二章

这篇时间较之前篇章时间靠后,是由于,某一天晚上看完Java编程思想文献之后来不及做笔记了. 以下笔记基本为转载,不是原创 第二章   一切都是对象 目录: 2.1 用引用操纵对象 2.2 必须由你创建所有对象 2.3 永远不需要销毁对象 2.4 创建新的数据类型:类 2.5 方法.参数和返回值 2.6 构建一个Java程序 2.7 你的第一个Java程序 2.8 注释和嵌入式文档 2.9 编码风格 2.1 用引用操纵对象 一切都看作对象,操纵的标识符实际上是对象的一个“引用”,遥控器(引用)操纵

[书籍翻译] 《JavaScript并发编程》 第二章 JavaScript运行模型

本文是我翻译<JavaScript Concurrency>书籍的第二章 JavaScript运行模型,该书主要以Promises.Generator.Web workers等技术来讲解JavaScript并发编程方面的实践. 完整书籍翻译地址:https://github.com/yzsunlei/javascript_concurrency_translation .由于能力有限,肯定存在翻译不清楚甚至翻译错误的地方,欢迎朋友们提issue指出,感谢. 本书第一章我们探讨了JavaScri

对一千万条数据进行排序---编程珠玑第二版 第一章

本书第一章提出了一个看似简单的问题,有最多1000万条不同的整型数据存在于硬盘的文件中,如何在1M内存的情况下对其进行尽可能快的排序. 每个数字用4byte,1M即可存储250 000个数据,显然,只要每次对250 000个数据排序,写入到文件中即可,重复40次. 那么如何选出每次遍历的二十五万条数据呢?有如下两个策略: 1.对一千万条数据遍历40次,第i次遍历时,判断数是否属于[i*250000,i*250000+249999),如果是,则读入内存,当第i次遍历完成时,内 存中有了二十五万条数

编程珠玑第一章习题答案

习题 1.1      如果不缺内存,如何使用一个具有库的语言来实现一种排序算法? 因为C++有sort,JAVA也有,这里以C++为例给出,记住如果用set集合来排序时,是不可以有元素重复的 代码: #include <iostream> #include <cstring> #include <cmath> #include <queue> #include <stack> #include <list> #include <

java编程思想--第二章 一切都是对象

如果不做引申的话,本章没有多少可总结的内容.有些以前没有注意的细节可以写下来. 1.数据存储的位置 (1).寄存器.程序控制不了. (2).堆栈.对象的引用.基本类型. (3).堆.各种对象. (4).常量存储.和代码在一起. (5).非RAM存储.主要指流对象和持久化对象,前者准备网络传输,后者被丢到了硬盘上. 2.java中的数组会被自动初始化: (1).基本类型数组元素会被初始化为0 (2).引用类型数组元素会被初始化为null 3.变量作用域 (1).基本类型的跟C一样 (2).引用类型

Python核心编程_第二章课后习题

以下是自己在学习Python核心编程时,做的课后练习题.现在把它们贴出来,以记录自己的学习过程.小弟是机械出身,很多练习题目写的很是机械.虽然写出来的脚本都能满足题目要求,但效率可能不是最好的,所以,小弟还是厚着脸皮把它们给贴出来,一来可以让高手指点,二来可以与我一样在学习Python的兄弟共同学习. 以下的程序均以题目标号命名,如2-3这个题目,程序名就为2_3.py. 习题2_3.py #!/usr/bin/env python A = 10 B = 4 print "A plus B is

python核心编程2第二章课后练习

2-1 print来显示变量的内容,仅用变量名时,输出的字符串使用单引号括起来的,这是为了让非字符串对象也能以字符串的方式显示在屏幕上,print语句使用str()函数显示对象,交互解释器调用repr()函数来显示对象 2-2 (a)运算1+2*4 (b)只会做运算不会输出 (c)运算未显示结果 (d)交互解释器输入一段语句后会返回语句结果 (e)print ‘1+2*4’   2-3   2-4 (a) #!/usr/etc/env python string =raw_input("plea