05 习题2.2 数组循环左移《PTA浙大版《数据结构(第2版)》题目集》
1.原题链接
2.题目描述
本题要求实现一个对数组进行循环左移的简单函数:一个数组 a a a中存有 n n n( > 0 >0 >0)个整数,在不允许使用另外数组的前提下,将每个整数循环向左移 m m m( ≥ 0 \ge 0 ≥0)个位置,即将 a a a中的数据由( a 0 a 1 ⋯ a n − 1 a_0 a_1 \cdots a_{n-1} a0a1⋯an−1)变换为( a m ⋯ a n − 1 a 0 a 1 ⋯ a m − 1 a_{m} \cdots a_{n-1} a_0 a_1 \cdots a_{m-1} am⋯an−1a0a1⋯am−1)(最前面的 m m m个数循环移至最后面的 m m m个位置)。如果还需要考虑程序移动数据的次数尽量少,要如何设计移动的方法?
输入格式:
输入第1行给出正整数 n n n( ≤ 100 \le 100 ≤100)和整数 m m m( ≥ 0 \ge 0 ≥0);第2行给出 n n n个整数,其间以空格分隔。
输出格式:
在一行中输出循环左移 m m m位以后的整数序列,之间用空格分隔,序列结尾不能有多余空格。
输入样例:
8 3
1 2 3 4 5 6 7 8
输出样例:
4 5 6 7 8 1 2 3
3.参考答案
第二章的内容为复习C语言基础,不涉及数据结构相关知识。
#include <stdio.h>
#define MAXN 100
#define Swap(a,b) a ^= b, b ^= a, a ^= b;
void ArrayShift( int Array[], int N, int M );
int main(){
int a[MAXN], n, m;
int i;
scanf("%d %d", &n, &m);
for ( i = 0; i < n; i++ ) scanf("%d", &a[i]);
m %= n;
ArrayShift(a, n, m);
printf("%d",a[0]);
for(i=1;i<n;i++)
printf(" %d",a[i]);
printf("\n");
return 0;
}
void ArrayShift( int Array[], int N, int M ){
int i,j;
if( M>0 && M<N ){
for(i=0, j= N-1; i<j; i++, j--)
Swap(Array[i], Array[j]);
for(i=0, j= N-M-1; i<j; i++, j--)
Swap(Array[i], Array[j]);
for(i=N-M, j= N-1; i<j; i++, j--)
Swap(Array[i], Array[j]);
}
}
4.解题思路
算法一
将第一个数组元素放到数组最后面,其余所有数组元素向左挪动一个相邻位置,挪动 m m m次。举例如下表所示。
初始数组 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
第一次循环 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 1 |
第二次循环 | 3 | 4 | 5 | 6 | 7 | 8 | 1 | 2 |
第三次循环 | 4 | 5 | 6 | 7 | 8 | 1 | 2 | 3 |
算法二
将数组整体前后翻转,再将前后两部分各自分别翻转。举例如下表所示。
初始数组 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
将数组整体前后翻转 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
前后两部分分别翻转 | 4 | 5 | 6 | 7 | 8 | 1 | 2 | 3 |
5.答案详解
#include<stdio.h>
#define MAXN 100
#define Swap(a,b) a ^= b, b ^= a, a ^= b;
//算法一
void ArrayShift1( int Array[], int N, int M );
//算法二
void ArrayShift2( int Array[], int N, int M );
int main(){
int m,n,i;
scanf("%d %d",&n,&m);
int a[n];
m=m%n;
//初始化数组
for(i=0;i<n;i++)
scanf("%d",&a[i]);
//调用函数让数组循环左移m位
//ArrayShift1(a,n,m);
ArrayShift2(a,n,m);
//单独输出第一个元素,让结尾无空格
printf("%d",a[0]);
for(i=1;i<n;i++)
printf(" %d",a[i]);
return 0;
}
// void ArrayShift1( int a[], int n, int m ){
// int temp,i,j;
// //循环左移m位
// for(i=0;i<m;i++){
// temp=a[0];//把第一个元素交给临时变量
// //将其余每个元素向左挪动1个位置
// for(j=0;j<n;j++)
// a[j]=a[j+1];
// //把本轮循环原本第一个元素放到数组最后
// a[n-1]=temp;
// }
// }
void ArrayShift2( int Array[], int N, int M ){
int i,j;
if( M>0 && M<N ){
for(i=0, j= N-1; i<j; i++, j--) /* 逆转N个数据 */
Swap(Array[i], Array[j]);
for(i=0, j= N-M-1; i<j; i++, j--) /* 逆转前N-M个数据 */
Swap(Array[i], Array[j]);
for(i=N-M, j= N-1; i<j; i++, j--) /* 逆转后M个数据 */
Swap(Array[i], Array[j]);
}
}
6.知识拓展
在本题答案中通过3次异或运算交换两个变量的值a ^= b, b ^= a, a ^= b;
。
以下是关于位运算的基础知识
二进制、位、字节
-
二进制:日常生活都是基于数字10来书写数字,例如日常说的2333是十进制的,可以写为2×10^3+ 3×10^2+ 3×10^1+ 3×10^0。这种书写数字的方法是基于10的幂,所以称以10为基底书写2333。计算机适用基底为2的数制系统,它用2的幂而不是10的幂,以2为基底表示的数字被称为二进制数(binary number)。二进制是计算技术中广泛采用的一种数制,二进制数据是用0和1两个数码来表示的数,进位规则是“逢二进一”。
-
例:二进制数1101可表示为:
1×23+ 1×22+ 0×21+ 1×20
以十进制数表示为:
1×8 + 1×4 + 0×2 + 1×1 = 13
-
位:二进制计数系统中,位简记为b,也称为比特,每个二进制数字0或1就是一个位(bit)。
-
字节:位是数据存储的最小单位,其中8 bit 就称为一个字节(Byte)。
位编号 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
二进制数 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
位值 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255
因此,1字节可储存0 ~255范围内的数字,总共256个值。或者,通过不同的方式解释位组合 (bit pattern),程序可以用1字节储存-128~+127范围内的整数,总共还是 256个值。例如,通常unsigned char用1字节表示的范围是0~255,而signed char用1字节表示的范围是-128~+127。
八进制
-
计算机也通常使用八进制和十六进制,因为8和16都是2的幂,八进制和十六进制比十进制更接近计算机的二进制系统。
-
八进制(octal)是指八进制记数系统。该系统基于8的幂,用0~7表示数字(正如十进制用0~9表示数字一样)。
-
例如,八进制数451(在C中写作0451,前面的0表示这是一个八进制数)表示为:4×8^2+ 5×8^1+ 1×8^0。
-
因为2的3次方等于8,所以每个八进制位对应3个二进制位。
八进制位 0 1 2 3 4 5 6 7 等价二进制位 000 001 010 011 100 101 110 111 -
例:八进制数0173的二进制数01111011。
八进制位 0173 1 7 3 等价二进制位 01111011 001 010 011
十六进制
-
十六进制(hexadecimal或hex)是指十六进制记数系统。该系统基于16 的幂,用0~15表示数字。由于没有单独的数表示10~15,所以用字母A~F来表示。
-
例如:十六进制数 A3F(在C中写作0xA3F,前面的0x表示这是一个十六进制数)表示为: 10×162+3×161+15×16^0。
-
因为2的4次方等于16,所以每个十六进制位对应4个二进制位。
十进制 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 十六进制 0 1 2 3 4 5 6 7 8 9 A B C D E F 等价二进制位 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 -
例:十六进制数0xAC2的等价二进制数101011000010。
十六进制位 0xAC2 A C 2 等价二进制位 101011000010 1010 1100 0010
C按位逻辑运算符
4个按位逻辑运算符都用于整型数据,之所以叫作按位 (bitwise)运算,是因为这些操作都是针对每一个位进行,不影响它左右两边的位。
二进制反码或按位取反:~
一元运算符~把1变为0,把0变为1。如下例子所示:
表达式 :~(10011010)
结果值 :(01100101)
为了理解位运算,这里用二进制计数法写出值,但实际的C程序中不这样使用二进制数。
假设变量a的类型是int,已被赋值为2,在二进制中,00000010表示2。那么,~a的值是11111101,即253。
注意,该运算符不会改变a的值,就像3 * a不会改变a的值一样, a仍然是2。但是,该运算符确实创建了一个可以使用或赋值的新值。
#include <stdio.h>
int main() {
unsigned char a = 2;
unsigned char b;
b = ~a;
printf("%d", b);
return 0;
}
按位与:&
二元运算符&通过逐位比较两个运算对象,生成一个新值。对于每个 位,只有两个运算对象中相应的位都为1时,结果才为1。
从真/假方面看,只有当两个位都为真时,结果才为真。
如下例子所示:
表达式 :(10010011) & (00111101)
,二进制10010011是十进制147,二进制00111101是十进制61。
位编号 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
第一个二进制数 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 1 |
第二个二进制数 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 1 |
按位与运算结果 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 |
由于两个运算对象中编号为4和0的位都为1,得:
结果值 :(00010001) 二进制00010001是十进制17
#include <stdio.h>
int main() {
unsigned char a = 147;
unsigned char b = 61;
printf("%d", a & b);
return 0;
}
按位或:|
二元运算符|,通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中相应的位为1,结果就为1。
从真/假方面看,如果两个运算对象中相应的一个位为真或两个位都为真,那么结果为真。
如下例子所示:
表达式 :(10010011) || (00111101)
,二进制10010011是十进制147,二进制00111101是十进制61。
除了编号为6的位,这两个运算对象的其他位至少有一个位为1,得:
结果值:(10111111)二进制10111111是十进制191
#include <stdio.h>
int main() {
unsigned char a = 147;
unsigned char b = 61;
printf("%d", a | b);
return 0;
}
按位异或:^
二元运算符^
逐位比较两个运算对象。对于每个位,如果两个运算对象中相应的位一个为1,另一个为0,运算结果为1。
从真/假方面看,如果两个运算对象中相应的一个位为真且不是两个位同为1,那么结果为真)。
如下例子所示:
表达式 :(10010011)^(00111101)
,二进制10010011是十进制147,二进制00111101是十进制61。
编号为0的位都是1,所以结果为0,得:
结果值 :(10101110) ,二进制10101110是十进制174
#include <stdio.h>
int main() {
unsigned char a = 147;
unsigned char b = 61;
printf("%d", a ^ b);
return 0;
}
C有一个按位异或和赋值结合的运算符:^=。下面两条语句产生的最终作用相同:
a^= 123;
a = a ^ 123;
在本题答案中通过3次异或运算交换两个变量的值a ^= b, b ^= a, a ^= b;
.
举例分析两个变量值的交换过程
位编号 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
a的值 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 1 |
b的值 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 1 |
第一步a ^= b后a的值 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 0 |
第二步b ^= a后b的值 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 1 |
第三步a ^= b后a的值 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 1 |
a ^= b后a的位记录了ab相同或不同的情况,a为1的位表示原本a和b在该位不同,a为0的位表示原本a和b在该位相同。
b ^= a后b的位根据a的记录情况变化,在a为1的位,b的对应位值变为相反的值,在a为0的位,b的对应位值不变。此时b变为原来a的值。
a ^= b后a的位根据a的记录情况在b的基础上变化,在a为1的位,a的位值变为与此时b相反的值,在a为0的位,a的位值不变。此时a变为原来b的值。
C按位逻辑运算符用法
掩码
例. 假设定义符号常量MASK为2 (即,二进制形式为00000010),只有1号位是1,其他位都是0。下面的语句:
flags = flags & MASK;
把flags中除1号位以外的所有位都设置为0,因为使用按位与运算符 (&)任何位与0组合都得0。
1号位的值不变(如果1号位是1,那么 1&1得 1;如果 1号位是0,那么 0&1也得0)。
这个过程叫作“使用掩码”,因为掩 码中的0隐藏了flags中相应的位。
用&=运算符可以简化前面的代码,如下所示:
flags &= MASK;
下面这条语句是按位与的一种常见用法:
ch &= 0xff; /* 或者 ch &= 0377; */
oxff的二进制形式是11111111,八进制形式是0377。这个掩码保持ch中最后8位不变,其他位都设置为0。无论ch原来是8位、16位或是 其他更多位,最终的值都被修改为1个8位字节。
打开位
例如,为了打开内置扬声器,必须 打开 1 号位,同时保持其他位不变。这种情况可以使用按位或运算符 (|)
flags = flags | MASK; //假设定义符号常量MASK为2
把flags的1号位设置为1,且其他位不变,因为使用 | 运算符,任何位与0 组合,结果都为本身,任何位与1组合,结果都为1。
例如,假设flags是00001111,MASK是10110110。下面的表达式:
flags | MASK
即是: (00001111) | (10110110) // 表达式
其结果为: (10111111) // 结果值
MASK中为1的位,flags与其对应的位也为1。MASK中为0的位,flags与其对应的位不变。
用|=运算符可以简化上面的代码,如下所示:
flags |= MASK;
关闭位(清空位)
例如,要关闭变量flags中的1号位。
flags = flags & ~MASK;//假设定义符号常量MASK为2
因为MASK除1号位为1以外,其他位全为0,所以~MASK除1号位为0 以外,其他位全为1。
使用&,任何位与1组合都得本身,所以这条语句保持 1 号位不变,改变其他各位。
另外,使用&,任何位与0组合都的0。所以无论1号位的初始值是什么,都将其设置为0。
例如,假设flags是00001111,MASK是10110110。下面的表达式:
flags & ~MASK
即是: (00001111) & ~(10110110) // 表达式
其结果为: (00001001) // 结果值
MASK中为1的位在结果中都被设置(清空)为0。
flags中与MASK为0的位相应的位在结果中都未改变。
可以使用下面的简化形式:
flags &= ~MASK;
切换位
切换位指的是打开已关闭的位,或关闭已打开的位。可以使用按位异或 运算符(^)切换位。
也就是说,假设b是一个位(1或0),如果b为1,则 1^b
为0;如果b为0,则1^b
为1。
另外,无论b为1还是0,0^b均为b。
因此,如果使用^组合一个值和一个掩码,将切换该值与MASK为1的位相对应的 位,该值与MASK为0的位相对应的位不变。
要切换flags中的1号位,可以使用下面两种方法
flags = flags ^ MASK;
flags ^= MASK;
例如,假设flags是00001111,MASK是10110110。表达式:
flags ^ MASK
即是: (00001111) ^ (10110110) // 表达式
其结果为: (10111001) // 结果值
flags中与MASK为1的位相对应的位都被切换了,MASK为0的位相对应的位不变。
检查位的值
例如,flags中 1号位是否被设置为1?不能这样直接比较flags和MASK: if (flags == MASK)
这样做即使flags的1号位为1,其他位的值会导致比较结果为假。
因此, 必须覆盖flags中的其他位,只用1号位和MASK比较:
if ((flags & MASK) == MASK)
由于按位运算符的优先级比==低,所以必须在flags & MASK周围加上圆括号。
为了避免信息漏过边界,掩码至少要与其覆盖的值宽度相同。
位移运算符
左移:<<
左移运算符(<<)将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端位的值丢失,用0填充空出的位置。
下面的例子中,每一位都向左移动两个位置:
(10001010) << 2 // 表达式
(00101000) // 结果值
该操作产生了一个新的位值,但是不改变其运算对象。
例如,假设 a为1,那么 a<<2为4,但是a本身不变,仍为1。
#include <stdio.h>
int main() {
unsigned char a = 1;
printf ("%d", a << 2);
return 0;
}
可以使用左移赋值运算符(<<=)来更改变量的值,该运算符将变量中的位向左移动其右侧运算对象给定值的位数。
#include <stdio.h>
int main() {
unsigned char a = 1;
a<<=2;
printf ("%d", a);
return 0;
}
右移:>>
右移运算符(>>)将其左侧运算对象每一位的值向右移动其右侧运算 对象指定的位数。左侧运算对象移出右末端位的值丢。对于无符号类型,用 0 填充空出的位置;对于有符号类型,其结果取决于机器。空出的位置可用 0填充,或者用符号位(即,最左端的位)的副本填充:
(10001010) >> 2 // 表达式,有符号值
(00100010) // 在某些系统中的结果值
(10001010) >> 2 // 表达式,有符号值
(11100010) // 在另一些系统上的结果值
下面是无符号值的例子:
(10001010) >> 2 // 表达式,无符号值
(00100010) // 所有系统都得到该结果值
每个位向右移动两个位置,空出的位用0填充。
右移赋值运算符(>>=)将其左侧的变量向右移动指定数量的位数。如下所示:
#include <stdio.h>
int main() {
unsigned char a = 16;
a>>=3;
printf ("%d", a);
return 0;
}
移位运算符用法
例1
移位运算符针对2的幂提供快速有效的乘法和除法:
number << n number乘以2的n次幂
number >> n 如果number为非负,则用number除以2的n次幂
这些移位运算符类似于在十进制中移动小数点来乘以或除以10。
例2
移位运算符还可用于从较大单元中提取一些位。例如,假设用一个unsigned long类型的值表示颜色值,低阶位字节储存红色的强度,下一个字节储存绿色的强度,第 3 个字节储存蓝色的强度。随后你希望把每种颜色的强度分别储存在3个不同的unsigned char类型的变量中。
#define BYTE_MASK 0xff
unsigned long color = 0x002a162f;
unsigned char blue, green, red;
red = color & BYTE_MASK;
green = (color >> 8) & BYTE_MASK;
blue = (color >> 16) & BYTE_MASK;
以上代码中,使用右移运算符将 8 位颜色值移动至低阶字节,然后使用掩码技术把低阶字节赋给指定的变量。