一道简单的算法题:不借助第三变量来交换两个变量的值

今天做笔试碰到一道简单的算法题:不借助第三变量来交换两个变量的值,记录一下。

交换两个变量的值的普遍做法都是借助第三变量,这样具有较高的可读性。

a = 3
b = 5

t = a
a = b
b = t

但是,如果内存有限,只允许用2个变量呢?
强大的CS当然有办法解决,不过可能要利用指针、位运算之类的技巧。

1.算术运算:利用两数之和(差)减去另一个数

原理:把a、b看做数轴上的点,围绕两点间的距离 a b s ( a − b ) abs(a-b) abs(ab) 来进行计算。
思路:先把两数之和保存在其中一个变量 a 中,然后用和减去另一个变量 b,b= b 原 b_原 b,用这个差覆盖这个变量b,此时 b 就拥有原来 a 的值(b= a 原 a_原 a);接着,用和与新的 b 的差来覆盖 a,新的 b 等于原来的 a,那么 a 就拥有原来 b 的值(a= b 原 b_原 b),从而达到交换变量的值的目的。

1.1 和:

a = 3
b = 5
a = a + b  # 和; 运算后,a = 8, b = 5
b = a - b  # b_new == a_ori; a=8, b=3
a = a - b  # a_new == b_ori; a=5, b=3,达到交换变量的目的

依样画葫芦,当然可以借助两数之差来实现,思路与上面类似。

1.2 差:

a = 3
b = 5
a = a - b  # 差
b = b + a  # b_new == a_ori
a = b - a  # a_new == b_ori

1.3 乘除也是可能的。

优点:此算法与标准算法相比,多了三个计算的过程,但是没有借助临时变量。(以下称为算术算法)
缺点:只能用于数字类型,字符串之类的就不可以了。a+b有可能溢出(超出int的范围),溢出是相对的, +了溢出了,-回来不就好了,所以溢出不溢出没关系,就是不安全。
因为:a + b 向上溢出后,后面的两次 a - b 又会向下溢出,又溢回来了:)
而MSDN也说得很清楚:对于不用任何 checked 或 unchecked 运算符或语句括起来的非常数表达式(在运行时计算的表达式),除非外部因素(如编译器开关和执行环境配置)要求 checked 计算,否则默认溢出检查上下文为 unchecked。
即这种情况下默认是不检查溢出的,如果我们实在担心外部因素,大不了加个unchecked:

unchecked
{
  a = a + b;
  a = a - b;
  a = a - b;
}

所以:这个方法也是没问题的。

2.位运算 ^ (异或):

   异或的特点是:一个数据a与另一个数据b做异或运算之后,变成了另外一个数c,再读取这个数据就不是原来的数据了,我们如果再拿这个数据c和数据b异或一次,这个数据又变回原来的数据a。**这里用的实际上是位运算的异或。**
a=3
b=5
a = a^b  # 异或
b = a^b  # b_new == a_ori
a = a^b  # a_new == b_ori
a = 3
b = 5
b = a^b  # 异或
a = b^a  # a_new == b_ori
b = b^a  # b_new == a_ori

如果异或用于或非表示的话,就是:a ^ b = (a or b) and (!a or !b)。
Python中,
(1)位运算与或非用以下符号表示:

&,按位与
|,按位或
~,按位非

(2)逻辑运算与或非用以下符号表示:

and,逻辑与
or,逻辑或
not,逻辑非

两个数作异或,其实就是用两个数的二进制形式作位运算,相同(都为0或1)就得到0,不同为1。因此,需要用位运算的与或非这一套符号

因此,把上面的异或展开成与或非的表示,可得

a = 3
b = 5
a = (a | b) & (~a | ~b)    # 想想异或为什么要同时满足两个条件?
b = (a | b) & (~a | ~b)
a = (a | b) & (~a | ~b)
a = 3
b = 5
b = (a | b) & (~a | ~b)    # 想想异或为什么要同时满足两个条件?
a = (b | a) & (~b | ~a)
b = (b | a) & (~b | ~a)

3. 如果用C语言,那么还可以用指针

对地址的操作实际上进行的是整数运算,比如:
(1)两个地址相减得到一个整数,表示两个变量在内存中的储存位置隔了多少个字节(位移)
(2)地址和一个整数相加,如“a+10”,表示以a为基地址,在a后10个a类数据单元的地址
所以理论上可以通过和算术算法类似的运算来完成地址的交换,从而达到交换变量的目的。即:

int *a,*b; //假设
*a=new int(10);
*b=new int(20); //&a=0x00001000h,&b=0x00001200h
a=(int*)(b-a); //&a=0x00000200h,&b=0x00001200h
b=(int*)(b-a); //&a=0x00000200h,&b=0x00001000h
a=(int*)(b+int(a)); //&a=0x00001200h,&b=0x00001000h

通过以上运算a、b的地址真的已经完成了交换,且a指向了原先b指向的值,b指向原先a指向的值了吗?上面的代码可以通过编译,但是执行结果却不对!

原因何在?

(1)首先必须了解,操作系统把内存分为几个区域:系统代码/数据区、应用程序代码/数据区、堆栈区、全局数据区等等。在编译源程序时,常量、全局变量等都放入全局数据区,局部变量、动态变量则放入堆栈区。

(2)当算法执行到“a=(int*)(b-a)”时,a的值并不是0x00000200h,而是要加上变量a所在内存区的基地址,实际的结果是:0x008f0200h,其中0x008f即为基地址(基地址可以看成坐标轴原点),0200即为a在该内存区的位移。它是由编译器自动添加的。【局部变量的地址为所在内存区的基地址+变量在该内存区的偏移量。具体原理见文末的注释。
因此导致以后的地址计算均不正确,使得a,b指向所在内存区的其他内存单元。

(3)再次,地址运算不能出现负数,即当a的地址大于b的地址时,b-a<0,系统自动采用补码的形式表示负的位移,由此会产生错误,导致与前面同样的结果。**

有办法解决吗?当然!以下是改进的算法:

int *a,*b; //假设
*a=new int(10);
*b=new int(20); //&a=0x00001000h,&b=0x00001200h

if (a < b)
{
a = (int*)(b-a)
b = (int*)( b - (int(a)&0x0000ffff) )
a = (int*)( b +(int(a)&0x0000ffff) )
}
else
{
b = (int*)(a - b)
a = (int*)(a - (int(b)&0x0000ffff))
b = (int*)(a + (int(b)&0x0000ffff))
}

算法做的最大改进就是采用位运算中的与运算“int(a)&0x0000ffff”,因为地址中高16位为段地址,后16位为位移地址将它和 0x0000ffff 进行与运算后,段地址被屏蔽,只保留位移地址。这样就与原始算法吻合,从而得到正确的结果。
【段地址、基地址的区别见文末注释】
【为什么是16位?因为这里用16进制,16进制的1位可以表示的数字的数目等同于二进制的4位可以表示的数字的数目, 1 6 1 = 2 4 16^1=2^4 161=24,所以16进制的4位就等价于2进制的16位
【与运算的威力可见一斑,还有移位运算,左移一位等于乘以2,右移一位等于除以2······本科的数电模电要好好学】

此算法同样没有使用第三变量就完成了值的交换,与算术算法比较它显得不好理解,但是它有它的优点,即在交换很大的数据类型时(比如具有10000位的数字),执行速度比算术算法快。因为它交换的是地址,而变量值在内存中是没有移动过的。【指针的威力】

4. 栈实现

栈(stack)的特点是 last in first out(LIFO),而队列的特点是 first in first out(FIFO)。
栈实现真妙!

{
stack S;
push(S,x);
push(S,y);
x=pop(S);
y=pop(S);
}

5. 一行代码解决问题

b = (a + b) - (a = b);

假如x和y是字符串:string x = “x”,y = “y”;

1可以改装成:
x = x + y;
y = x.Substring(0, x.Length - y.Length);
x = x.Substring(y.Length);
5可以改装成:
x = y + (y = x).Substring(0, 0);
或:
x = y + (y = x) == “” ? “” : “”;

总结:算术算法和位算法计算量相当,地址算法中计算较复杂,却可以很轻松的实现大类型(比如自定义的类或结构)的交换,而前两种只能进行整形数据的交换(理论上重载“^”运算符,也可以实现任意结构的交换)

注:

  1. 汇编中段地址基地址是什么意思 ?
    段地址其实就是一种基地址,但基地址并不等于就是段地址。
    所谓基地址,顾名思义就可以理解为基本地址,他是相对偏移量的计算基准。 【变量地址=基地址+相对偏移量】
    (1)在实模式下,通常都是以段+偏移来定位地址,因此说,这时,段地址是基地址的一种
    (2)但是在堆栈上,常常不以ss寄存器来作为定址基准,而是经常用bp寄存器来定址,因此,此时堆栈段的段址就不能说是基地址 。
    (3)而保护模式下,不再有“段”的概念,这时的段寄存器里保存的是“段选择子”,和基地址根本就是两回事。

  2. 偏移地址是什么?
    偏移地址就是计算机里的内存分段后,在段内某一地址相对于段首地址(段地址)的偏移量. 如8086存储系统中 20位的物理地址(就是数据存储的实际地址)=16位的段基地址*16+16位的偏移量。
    为什么有偏移地址?
    由于8086/8088CPU内部的ALU只能进行16位的运算,而8086/8088有20条地址线,直接寻址能力1MB。因此,8086/8088所使用的20位物理地址,是由相应的段地址加上偏移地址组成的。【由于内存往往大于处理器的运算速率,例如目前常见的16G内存+2.4G/s主频的CPU。】
    原理:
    8086/8088有20条地址线,它的直接寻址能力为1MB。也就是在一个系统中可以有多达1MB的存储器,地址从00000H—FFFFFH。给定任意一个20位物理地址,就可以从中取出需要的指令和操作数。但是8086/8088CPU只能进行16位运算。与地址有关的寄存器SP、IP、BP、SI、DI也都是16位的,所以对地址的运算也只能是16位的。对于8086/8088来说,无论采用哪种寻址方式,寻找操作数的范围最大是2^16,也就是64K。如何才能形成20位的物理地址呢。系统先将1MB存储器以64KB为范围分成若干段。在寻址一个具体物理地址时,由一个基本地址再加上由SP或IP等可由CPU处理的16位偏移量来形成20位物理地址。
    当系统需要产生一个20位地址的时候,一个段寄存器会自动被选择。且自动左移4位再与一个16位地址偏移量相加产生所需的20位地址 [1] 。
    例如:数据段DS寄存器的值=0088H
    偏移地址=22H
    那么生成的20位物理地址等于 00880H+22H=008A2H

3. 那缓存(cache)又是什么东西?
 高速缓存(Cache)主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾而存在, 是CPU与内存之间的临时存贮器,容量小,但是交换速度比内存快。
 CPU要读取一个数据时,首先从Cache中查找,如果找到就立即读取并送给CPU处理;如果没有找到,就用相对慢的速度从内存中读取并送给CPU处理,同时把这个数据所在的数据块调入Cache中,可以使得以后对整块数据的读取都从Cache中进行,不必再调用内存。
  正是这样的读取机制使CPU读取Cache的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在Cache中,只有大约10%需要从内存读取。这大大节省了CPU直接读取内存的时间,也使CPU读取数据时基本无需等待。总的来说,CPU读取数据的顺序是先Cache后内存。

cache,中译名高速缓冲存储器,其作用是为了更好的利用局部性原理,减少CPU访问主存的次数。简单地说,CPU正在访问的指令和数据,其可能会被以后多次访问到,或者是该指令和数据附近的内存区域,也可能会被多次访问。因此,第一次访问这一块区域时,将其复制到cache中,以后访问该区域的指令或者数据时,就从cache中获取,就不用再从主存中取出。详情请查阅教程[5]。

[参考教程]
1.不用第三方变量如何交换两个数的值
2.C程序中交换两个变量数值,不使用第三方变量(四种方式)
3.Python运算符——菜鸟教程
4.关于不使用第三个变量交换2个变量的值
5.cache机制
6. 图解数据读写与Cache操作

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值