05 习题2.2 数组循环左移《PTA浙大版《数据结构(第2版)》题目集》

05 习题2.2 数组循环左移《PTA浙大版《数据结构(第2版)》题目集》

1.原题链接

习题2.2 数组循环左移 (pintia.cn)

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} a0a1an1)变换为( 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} aman1a0a1am1)(最前面的 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 位颜色值移动至低阶字节,然后使用掩码技术把低阶字节赋给指定的变量。

猜你喜欢

转载自blog.csdn.net/weixin_40171190/article/details/129739697