基于《算法与数据结构》链表的学习实现高精度计算

        本文章已使用Markdown重新编辑并修改文中错误,目前仅作留念,点击本段文字阅读最新版本。

        我们已经完成了《算法与数据结构》中线性表的学习,来看一个实际问题的应用。

        大家都知道C语言中int(整型)数据是有大小限制的,超出它的范围就无法正常运算,这时就有人说了,long int呢?还有long long呢?它们是比int数据范围要大,但是它们仍是有范围的,能不能实现完全不用考虑数据类型的范围限制,无论多大都能正常计算呢?

        有些同学可能已经了解过“高精度计算”了,它就是实现我们刚才的设想的方法,理论上(硬件支持的情况下)它是可以实现任意长的正数运算的计算器。但是有很多了解过的同学又懵了,因为多数高精度算法的实现都是通过数组来实现的,用链表来实现的太少见了,正好,作为线性表章节学习结束后对其性质的更深认识,我们拿链表来实现它。

一、程序设计要求

        设计程序,用链式存储结构实现两个任意长的整数的加、减、乘的运算问题。

二、要求分析

       题目内容十分简单,但是真正要实现可不一定容易。

        1、链表实现

        要求链表实现,我们就需要先考虑一下挑选哪一种链表。

        首先,拆输入内容的时候最先拆出来的是数字的最高位,链表输出的时候也应该先输出最高位,但是进行各种运算时又需要从最低位算起,也就是说,既有访问前驱的情况,也有访问后继的情况,那当然是直接上双向循环链表咯。

        2、任意长的整数

        既然已经说明int、long、long long有数据范围限制了,那自然是要换别的数据类型来存放输入的整数了,因为我们需要将输入的长整数的每一位拆成链表中的一个结点,正好,符合这些要求的一个数据类型出现了,字符串(字符型数组),可以每位拆开,长度可以自定,简直完美。

        3、加减乘运算

        既然我们把每一位都拆开了,那自然在进行四则运算时就不能简单的把它写成a+b了,而是要自己想办法去实现每一种运算。我们在此先不讨论除法的运算,因为要考虑到小数,实现较为麻烦。

三、程序设计思路及函数实现     

        1、双向循环链表结点定义及基础操作

         这里不做太多赘述,看不懂的话就需要回去重新温故一下了。

typedef struct Node{//双向循环链表结点定义
	int data;
	struct Node *next;
	struct Node *prior;
}Node,*LinkList;

LinkList InitList()//初始化链表
{
	LinkList L;
	L=(Node *)malloc(sizeof(Node));
	L->data=1;
	L->next=L;
	L->prior=L;
	return L;
}

int ListLength(LinkList L)//求表长
{
    LinkList p;
    int t=0;
    p=L->next;
    while(p!=L)
    {
        t++;
        p=p->next;
    }
    return t;
}

void CreateList_Head(LinkList L,int num)//头插法建表
{
	LinkList p,n;
	p=L;
	n=(Node *)malloc(sizeof(Node));
	n->data=num;
	n->next=p->next;
	n->prior=p;
	p->next=n;
	n->next->prior=n;
}

void CreateList_Tail(LinkList L,int num)//尾插法建表
{
	LinkList p,n;
	p=L;
	n=(Node *)malloc(sizeof(Node));
	n->data=num;
	n->next=p;
	n->prior=p->prior;
	p->prior->next=n;
	p->prior=n;
}

void DestroyList(LinkList L)//销毁表
{
	LinkList p,q;
	p=L->next;
	while(p!=L)
	{
		q=p->next;
		free(p);
		p=q;
	}
	free(L);
}

void PrintList(LinkList L) //输出链表
{
	LinkList p;
	if(!L->data)		//负数前的负号
		printf("-");
	p=L->next;
    while(p!=L)
    {
        printf("%d",p->data);
        p=p->next;
    }
	printf("\n");
}

        可以看到,这些基础操作基本和学到的是一样的,只是有一小部分根据实际问题做了修改,实际上,编程也是这样,大部分看似很难的要求都是通过各种基础操作经过修改后组合而实现的。

        2、数据输入+拆分

        我们需要输入一个字符串然后将其挨个拆成链表即可,需要注意的是,我们的操作数可能会出现负数的情况,数字可以读给链表,但"-"号该如何存下呢?如果认真看了上一段,其实里面已经透露了实现方法。我们在初始化链表时,为它开辟了一个头结点,那我们不妨把头结点的数据域给利用起来,将它当作一个标签值,当它值为1时,当前链表所表示的数字就是正数,而当它值为0时,链表所表示的就是负数咯。

#define MAXSIZE 1000
void GETNUM(LinkList L1,LinkList L2)//输入数字(字符串)并将其转换成链表
{
	char num[MAXSIZE];
	printf("输入操作数1:");
	gets(num);
	long long i;//使用vc6.0时将这个改成"_int64"
	for(i=0;i<strlen(num);i++)
	{
		if(num[i]=='-')
		{
			L1->data=0;			//链表头结点数据域表示正负(1为正,0为负)
			continue;
		}
		CreateList_Tail(L1,num[i]-'0'); //采用尾插法(确保高位在前)
	}
	printf("输入操作数2:");
	gets(num);
	for(i=0;i<strlen(num);i++)
	{
		if(num[i]=='-')
		{
			L2->data=0;
			continue;
		}
		CreateList_Tail(L2,num[i]-'0');
	}
}

        需要注意的是,读完"-"号以后记得跳过循环,不然会给链表读入奇怪的数据哦。

        还有注意读入数据时的num[i]-'0',可以思考一下为什么要这样写。

        3、加法实现(Add)

        好,这会算是到了核心部分了,分别是四种运算的实现。

        大家思考一下,在这样的数据结构上该如何实现加法呢?

        一位一位处理,是不是像极了我们小时候列竖式计算的方法,那不如我们就根据竖式找出运算规律,让计算机通过这样的规律来实现运算。首先来看加法。

623bbb5608de4332b4f7f9faeaa9cfd8.jpeg

        先来温习一下竖式计算,从中我们总结一下加法计算的注意点:

        ①低位加起

        ②注意进位(carry)

        我们先根据这两个注意点想一下设计的时的解决思路。要实现低位加起,即从链表表尾遍历到表头,也就是工作指针从L->prior(尾结点)开始,遍历条件为工作指针未指回L(头结点)时执行循环。

        还有注意进位,在列竖式计算时,我们都是求出和后把十位进上去,将个位加完上一位的进位后直接放在下面,这样的思路该如何实现呢。

52b83b15e57b4cd6bcbd9c07fb417db2.jpeg

根据图中信息及所学法则我们不难找出如下规律:

${r_1=(a_1+b_1+c_0){\;}MOD{\;}10}$

${r_2=(a_2+b_2+c_1){\;}MOD{\;}10}$

${r_3=(a_3+b_3+c_2){\;}MOD{\;}10}$

总结出一般规律:

${r_i=(a_i+b_i+c_{i-1}){\;}MOD{\;}10}$

其中

${c_{i}=\begin{cases}0 \; ,i=0\\\lfloor (a_{i}+b_{i}+c_{i-1})/10\rfloor \;,i>0 \end{cases}}$

在编程时我们不妨声明一个变量carry表示进位,sum表示两个数及上一位的进位之和,则有

sum=p->data+q->data+carry(old)

carry(new)=sum/10

需要注意的是,其中的old和new只是显式的表现一下此时进位的状态,也说明了这两句代码是有顺序的。此时我们还需要为结果开辟一个新链表,然后把求出来的一位存入结果链表中。即

CreateList_Head(Result,sum%10)

由于是从低位开始运算的,求得的结果也是先从低位开始,故我们使用头插法,保证高位在前的顺序。

        还有一个问题,两个操作数长度不同时循环提早结束了该如何继续处理。因为我们循环的执行条件只能是p!=L1&&q!=L2,这使得其中有一个跑完就结束了,可能还会存在一个链短先跑完了的情况,我们需要让没跑完的那个链跑完,继续让其执行上面的运算即可,不同的是,已经跑完的链就不用再加了,因为此时再往前跑它就没值了(或者说它的高位为0)。

例如L2链已跑完的情况,也就是说L1还没跑完,那么我们就可以在设一个循环使其循环条件为p!=L1,则有

sum=p->data+carry(old)

carry(new)=sum/10

        此时,我们已经解决了相加,进位的问题,貌似是已经可以实现两正整数的相加了。且慢,不知道大家有没有注意到我在上面给出的图中结果位数是大于两个操作数的位数的,也就是说,当两个链表跑完后,可能在较长的那个长整数最高位还有剩余的进位未处理,这个问题也需要解决,解决方法也很简单,只需要将剩余的进位直接插入结果链表即可,不然就会出现678+556=234的情况了。

        现在,我们可以说已经实现加法运算的主体了。算法如下:

LinkList Add(LinkList L1,LinkList L2)//加法实现
{
    LinkList Minus(LinkList,LinkList);
	LinkList p=L1->prior,q=L2->prior;//从链表尾端(低位)开始
	LinkList L3;
	int carry=0,sum;
	L3=InitList();
	while(p!=L1&&q!=L2)
	{
		sum=p->data+q->data+carry;
		CreateList_Head(L3,sum%10);  //采用头插法,低位先插入,仍需保证高位在前
		carry=sum/10;
		p=p->prior;
		q=q->prior;
	}
	while(p!=L1)
	{
		sum=p->data+carry;
		CreateList_Head(L3,sum%10);
		carry=sum/10;
		p=p->prior;
	}
	while(q!=L2)
	{
		sum=q->data+carry;
		CreateList_Head(L3,sum%10);
		carry=sum/10;
		q=q->prior;
	}
	if(carry)
	{
		CreateList_Head(L3,carry);
	}
	return L3;
}

        问题又又又来了,要是输入的操作数里有负数呢?毕竟我们在输入的时候都已经为负数的存在做了准备,那自然要考虑到负数的情况咯。

        情况1、正数加正数:直接用我们写的算法即可,因为我们写的就是针对这种情况的。

        情况2、正数加负数:可以转换成正数减正数的问题,即

${a+(-b)=a-b,(a,b>0)}$

        那我们直接可以使用减法运算来实现。

        情况3、负数加正数:同样也可以转换成正数(第二位)减正数(第一位)的问题,即

${-a+b=b-a,(a,b>0)}$

        同样我们仍然可以用减法运算来实现。

        情况4、负数加负数:我们可以将其转换为负的正数加正数,即

${-a+(-b)=-(a+b),(a,b>0)}$

        此时就可以直接套用我们已写出的算法了。

        不难发现,这些情况不需要再外加功能来实现,只需要在原本的功能中稍作修改即可。

以下是支持负数的加法实现:

LinkList Add(LinkList L1,LinkList L2)//加法实现
{
    LinkList Minus(LinkList,LinkList);
	LinkList p=L1->prior,q=L2->prior;//从链表尾端(低位)开始
	LinkList L3;
	int carry=0,sum;
	if((L1->data==1&&L2->data==1)||(L1->data==0&&L2->data==0))//当两数均为或均为负时直接相加
	{
		L3=InitList();
		while(p!=L1&&q!=L2)
		{
			sum=p->data+q->data+carry;
			CreateList_Head(L3,sum%10);  //采用头插法(保证高位在前)
			carry=sum/10;
			p=p->prior;
			q=q->prior;
		}
		while(p!=L1)
		{
			sum=p->data+carry;
			CreateList_Head(L3,sum%10);
			carry=sum/10;
			p=p->prior;
		}
		while(q!=L2)
		{
			sum=q->data+carry;
			CreateList_Head(L3,sum%10);
			carry=sum/10;
			q=q->prior;
		}
		if(carry)
		{
			CreateList_Head(L3,carry);
		}
	}
	else
	{
		if(L1->data==1&&L2->data==0)//正数加负数,转换为正数减正数
		{
			L2->data=1;     //先将其置为正数,防止减法运算时负号判断问题
			L3=Minus(L1,L2);
			L2->data=0;		//恢复原来的符号
		}
		else//负数加正数,转换为第二个数减第一个数
		{
			L1->data=1;
			L3=Minus(L2,L1);
			L1->data=0;
		}
	}
	if(L1->data==0&&L2->data==0)//两负数之和仍为负数
		L3->data=0;
	return L3;
}

        代码中需要注意的点是,符号的转换,大家应该会注意到里面存在L->data标签值反复横跳的情况,这是为什么呢?因为我们说了,其他情况要进行转换才能计算,符号不转换,就会出现违背运算法则的情况,比如试图让

${a+(-b)=a-(-b)=a+b,(a,b>0)}$

如此荒谬的定理出现。

那又为什么跳回来呢?因为我们不能改变原数字的值,后面还要进行运算呢,你把原数给改了,那就只能得到一个正确结果和三个错误结果了。

        3、减法实现(Minus)

        加法已经实现了,接下来该实现减法了,同样,我们先来看看竖式计算。

a3e3b06356434bab8b84e72167b70294.jpeg

        照着上面加法列出的注意事项,我们还是能列出两条:

        ①低位减起

        ②注意借位(borrow)

        好的一方面是结果计算出来后不存在往前进一位的情况,就是我们不需要再考虑进位了,但是,会出现相减时不够减的情况,此时我们就得向前一位借个1(借过来后相当于当前一位的10)来进行运算了,也就是说我们又需要考虑借位了。我们来看一下它的思路。

1a702893dee24fafa799ec4f6f380cef.jpeg

        同样,我们根据图中的计算思路来找规律:

${r_1=a_1-c_1-b_0+10b_1}$

${r_2=a_2-c_2-b_1+10b_2}$

${r_3=a_3-c_3-b_2+10b_3}$

        总结一般规律:

${r_i=a_i-c_i-b_{i-1}+10b_i}$

        其中

${b_i=\begin{cases} 0,\;\;a_{i}-c_{i}\geq 0\;or\;i=0 \\1,\;\;a_{i}-c_{i}< 0 \end{cases}}$

        接下来该编程实现了,在编写时为方便我们不妨设一个变量表示未加入当前一位的借位的差值difference用来判断当前一位是否需要借1,再设置一个变量borrow来表示借位情况。则有

difference=p->data-q->data-borrow(old)

borrow (new)=difference<0?1:0

        同样,式中old和new表明这两句是有顺序的。然后我们需要把当前位的计算结果插入结果链表中,即

CreateList_Head(L3,difference+10*borrow)

        由于刚才difference变量中并没有包含当前位借位情况,所以我们要在插入的时候记得把借位加进去,不然就会出现有其中某一位数成了负数的情况。

        在加法运算中我们还考虑到了一个问题,就是其中一个链先跑完的情况,在减法中,由于存在借位,依然要考虑这种问题,只是它的情况稍复杂些。

        此时我们有两个链表p(L1)和q(L2),它们分别表示两个数,当p链比q链长的时候,我们所想的“借位法”是可以计算得正常值的,但是当q链比p链长的时候,再全面一点,也就是q链表示的数绝对值大于p链所表示的数的绝对值时,两数的运算结果会是负数,这样的“借位法”是没办法使用的。但是大家都知道两数减得负数结果的数值和拿大数减小数的值是一样的,只是前面多了负号,既然如此,那我们就将这两种情况都转换为大减小,然后再去思考是否要加负号在前面,那又如何区分这两种情况呢?实际上我们只需要将两个链表所表示的数比较大小即可,比较过程又分出了以下两种情况:

        ①两数不同长:那必然是越长越大咯(注意我们此时只讨论其绝对值)

        ②两数同长:这个比较有点类似于字符串比较,即从高位比起,若出现数1的当前位大于数2的当前位,则数1大于数2。

        那我们不妨编写一个函数来判断两数绝对值的大小。

int ValueCompare(LinkList L1,LinkList L2)//绝对值比较函数,返回1为L1>L2,返回0为L2>L1
{
	if(ListLength(L1)>ListLength(L2))
		return 1;
	else
	{
		if(ListLength(L1)<ListLength(L2))
			return 0;
		else
		{
			LinkList p,q;
			p=L1->next;
			q=L2->next;
			while(p!=L1&&q!=L2)
			{
				if(p->data>q->data)
					return 1;
				else
                {
					if(p->data<q->data)
						return 0;
					else
                    {
                        p=p->next;
                        q=q->next;
                    }
                }
			}
		}
	}
	return 1;
}

        这样一来,我们就可以通过这个函数返回的值来得知两数绝对值的大小情况了。可是问题还没解决完,我们只考虑了在${min(ListLength(L1),ListLength(L2))}$范围内的借位问题,当借位情况跑出这个范围,比如在计算100000-1这种p链连续借位而q链已经结束的情况该如何解决?

        和加法一样,我们还是将剩余的链跑完,执行和原来相同的算法,只是不再考虑q链了。即

difference=p->data-borrow(old)

borrow(new)=difference<0?1:0

现在可以给出算法了:

LinkList Minus(LinkList L1,LinkList L2)//减法实现,
{
	LinkList p,q,p_head,q_head;
	LinkList L3;
	int borrow=0,difference,Positive;
	Positive=ValueCompare(L1,L2);//获取绝对值比较情况
	if(Positive)//如果L1大则让L1-L2
	{
		p=L1->prior;
		q=L2->prior;
		p_head=L1;
		q_head=L2;
	}
	else//如果L2大,则让L2-L1
	{
		p=L2->prior;
		q=L1->prior;
		p_head=L2;
		q_head=L1;
	}
	L3=InitList();
	while(p!=p_head&&q!=q_head)
	{
		difference=p->data-borrow-q->data;
		if(difference<0)
			borrow=1;
		else
			borrow=0;
		CreateList_Head(L3,difference+10*borrow);
		p=p->prior;
		q=q->prior;
	}
	while(p!=p_head)	//已经进行了大小比较,q所指链表一定先比p所指链表结束
	{
		difference=p->data-borrow;
		if(difference<0)
			borrow=1;
		else
			borrow=0;
		CreateList_Head(L3,difference+10*borrow);
		p=p->prior;
	}
	if(!Positive)
		L3->data=0;
	return L3;
}

        可以看到,其中添加了让p,q指向不同链表(数字)的操作,这样无论哪个链表更大,最后都只需要执行p-q即可,同时也将链表提前结束的情况限制在了q上,最后就只需要将p链表上剩余的数据处理完即可。

        同样,以上我们只实现了正数减正数的情况,对于输入数据存在负数的情况,我们仍然先分析一下:

        情况1、正数减负数:只需将其转化为正数加正数用加法计算即可。

${a-(-b)=a+b,\;(a,b>0)}$

        情况2、负数减正数:转换为两个负数相加,用加法计算即可。

${-a-b=-(a+b),\;(a,b>0)}$

        情况3、负数减负数:转换为正数减正数问题,再用减法计算即可。

${-a-(-b)=b-a,\;(a,b>0)}$

        以上我们就把减法可能遇见的问题解决完了,来看看完全版的减法运算实现吧。

LinkList Minus(LinkList L1,LinkList L2)//减法实现,
{
	LinkList p,q,p_head,q_head;
	LinkList L3;
	int borrow=0,difference,Positive;
	if(L1->data==1&&L2->data==1)//正数减正数
	{
		Positive=ValueCompare(L1,L2);//判断步骤确定结果的正负
		if(Positive)
		{
			p=L1->prior;
			q=L2->prior;
			p_head=L1;
			q_head=L2;
		}
		else
		{
			p=L2->prior;
			q=L1->prior;
			p_head=L2;
			q_head=L1;
		}
		L3=InitList();
		while(p!=p_head&&q!=q_head)
		{
			difference=p->data-borrow-q->data;
			if(difference<0)
				borrow=1;
			else
				borrow=0;
			CreateList_Head(L3,difference+10*borrow);
			p=p->prior;
			q=q->prior;
		}
		while(p!=p_head)	//已经进行了大小比较,q所指链表一定先比p所指链表结束
		{
			if(!borrow)
			{
				CreateList_Head(L3,p->data);
				p=p->prior;
			}
			else
			{
				difference=p->data-borrow;
				if(difference<0)
					borrow=1;
				else
					borrow=0;
				CreateList_Head(L3,difference+10*borrow);
				p=p->prior;
			}
		}
		if(!Positive)
			L3->data=0;
	}
	else
	{
		if(L1->data==1&&L2->data==0)//正数减负数,转换为正数加正数
		{
			L2->data=1;
			L3=Add(L1,L2);
			L2->data=0;
		}
		else
			if(L1->data==0&&L2->data==1)//负数减正数,转换为负的正数加正数
			{
				L1->data=1;
				L3=Add(L1,L2);
				L1->data=0;
				L3->data=0;
			}
			else//负数减负数,转换为第二个数减第一个数
			{
				L1->data=1;
				L2->data=1;
				L3=Minus(L2,L1);
				L1->data=0;
				L2->data=0;
			}
	}
	return L3;
}

        以上就是减法的实现了,代码中的每一句都是有它存在的意义的,看不懂可以试试删了,可能会出现意想不到的结果。

4、乘法实现(Multiply)

        同样,我们来看竖式。

ef963814cbf044a0bf60cdee122c6f55.jpeg

        还是需要注意从低位乘起和进位的问题,但是,不同的是,乘法的结果并不是直接得出的,而是乘完后又进行了加法运算,也就是说,乘法的运算步骤稍多一点。以下是计算模板,我们来找一下一般规律。

1fee674362a74b7a8d02b461a0097d2e.jpeg

        从图中看,有

${r_1=r_{11}=(a_1 \times b_1+c_{01}) \; MOD \; 10}$

其中

${c_{01}=0,c_0=0}$ 

${r_2=r_{21}+r_{12}\\ =\Big\{\big[(a_2 \times b_1+c_{11}) \; MOD\;10+(a_1 \times b_2+c_{02})\;MOD\;10\big]+c_1\Big\}\;MOD\;10}$

其中

${c_{11}=\big\lfloor (a_1 \times b_1+c_{01})/10 \big\rfloor ,c_{01}=0,c_{02}=0,c_1=0}$

${r_3=r_{31}+r_{22}+r_{13}\\ =\Big\{\big[(a_3 \times b_1+c_{21})\;MOD\;10+(a_2 \times b_2+c_{12})\;MOD\;10\\ +(a_1 \times b_3+c_{03})\;MOD\;10\big]+c_2\Big\}\;MOD\;10}$

其中

${c_{21}=\big\lfloor (a_2 \times b_1+c_{11})/10 \big\rfloor,c_{12}=\big\lfloor (a_1 \times b_2+c_{02})/10 \big\rfloor\\c_{03}=0,c_2=\big\lfloor (r_{21}+r_{12}+c_1)/10 \big\rfloor}$

        可以看出,有点复杂,但是把它总结一下,很容易得出以下一般规律:

${\displaystyle r_k=\Bigg[\sum_{i=1}^m\big(\sum_{j=1}^nr_{ij}\big)+c_{k-1}\Bigg]\;MOD\;10}$

${c_k=\begin{cases} 0,\;k=0\\\displaystyle \Bigg\lfloor \bigg[\sum_{i=1}^m\Big(\sum_{j=1}^nr_{ij}\Big)+c_{k-1}\bigg]/10 \Bigg\rfloor,\,k>1 \end{cases}}$

其中

${i+j=k+1}$

${m=ListLength(L1)+1,n=ListLength(L2)}$

${r_{ij}=\begin{cases}0\;,\;i=0\,or\,j=0\\(a_i \times b_j+c_{(i-1)j})\;MOD\;10 \end{cases}}$

${c_{ij}=\begin{cases} 0,\,i=0\\\big\lfloor(a_i \times b_j+c_{(i-1)j})/10 \big\rfloor,\;i,j>0 \end{cases}}$

        由于乘法的规律里面的条件及中间变量有点复杂,很容易看懵,但是看不懂没关系,因为接下来用不到(先别骂,学到就是赚到,学到就是赚到),倒是可以帮大家理解一下数组法的乘法算法。大家应该能注意到它的操作都需要关注每个数字在长整数中的位置,而这恰恰是单纯的链表无法实现的,也是多数人都选择数组实现的原因——随机访问的特性。

那既然这条路走不通,我们就换一条路走呗。

01b5d4385ec04b0b908ff003baaf3e87.jpeg

        如图,我们很容易看出中间的部分的第一行是上面每一位数乘以下面的最低位所得的,第二行又是上面每一位数乘以下面第二低位所得,以此类推,下面的数有几位,就能得到几行数,最后再将他们对齐相加即可获得结果,也就是说我们不找规律,老老实实让电脑用竖式法来实现,即先乘后加。

这时我们就只需要这两个公式了。

${r_{ij}=(a_i \times b_j+c_{(i-1)j})\;MOD\;10}$

${c_{ij}=\begin{cases} 0,\,i=0\\\big\lfloor(a_i \times b_j+c_{(i-1)j})/10 \big\rfloor,\;i,j>0 \end{cases}}$

         现在来编程实现吧,在编程时我们设置一个变量product来储存当前相乘的两位的乘积加上一位的进位,carry仍用来表示进位,则有:

product=p->data*q->data+carry(old)

carry(new)=product/10

        将其插入结果链表

CreatList_Head(Result,product%10)

        这里需要明确的是,乘法中的p指针和q指针的移动不是同步的,先由p指针从尾跑到头挨个与q所指元素相乘,q再前移,后p再轮一圈,也就是说这两个指针跑的时候由内外循环之分。还有需要注意的是外循环每循环一轮会得到一个乘数结果,这里的结果并非最终结果,而是上图红框中的中间结果其中的一行,所以要将外循环每轮获得的结果相加才能获得最终结果。

        内循环结束后还是判断有没有剩余进位,有则直接插入。

        此时问题来了,图中竖式的结果是中间结果竖着累加所得,每个数的最低位都对应在上一个数的第二低位上,但是我们求得的数字又该如何保证在相加的时候每位对应呢?实际上,将它们后面空着的地方用0填上大家就容易理解了,也就是求得结果的eq?10%5E%7Bn%7D倍,也就是往后面插n个0。那我们不妨定义一个外循环计次变量t,外循环每循环一圈就让它加一,然后往结果后添加t个0。

        现在思路已经很清晰了,我们来实现:

LinkList Multiply(LinkList L1,LinkList L2)//乘法实现,利用列竖式的思路
{
	LinkList p,q,temp_ele,temp_L3;
	LinkList L3=InitList();
	CreateList_Head(L3,0);
	int carry=0,product,i,t;
	q=L2->prior;
    t=0;
	while(q!=L2)
	{
		p=L1->prior;
		temp_ele=InitList();
		while(p!=L1)
		{
			product=p->data*q->data+carry;
			CreateList_Head(temp_ele,product%10);
			carry=product/10;
			p=p->prior;
		}
		if(carry)
		{
			CreateList_Head(temp_ele,carry);
			carry=0;
		}
		for(i=0;i<t;i++)//低位补0
		{
			CreateList_Tail(temp_ele,0);
		}
		temp_L3=L3;
		L3=Add(temp_ele,temp_L3);
		DestroyList(temp_L3);
		DestroyList(temp_ele);
		q=q->prior;
		t++;
	}
	return L3;
}

        代码中temp_ele表示每一个大循环得到的结果,即中间结果的其中一行,用于累加。

        其中需要注意的是每次的两个temp变量的销毁,因为在下一轮相乘时,如果旧链表未销毁或置空的话就会直接插在上一轮的结果里,导致运算错误。而temp_L3的销毁则是因为L3在叠加后指向了新的结果链表,若不将其原来的链表释放,会导致内存泄漏。

        同样,以上我们实现的只是正数与正数相乘的运算,而其它情况呢?

        情况2、正数乘负数

${a \times (-b)=-(a \times b),\;(a,b>0)}$

        情况3、负数乘正数

${-a \times b =-(a \times b),\;(a,b>0)}$

        情况4、负数乘负数

${-a \times (-b)=a \times b,\;(a,b>0)}$

         诶?结果的绝对值都是两数绝对值的乘积,那我们直接进行计算,最后直接根据两数符号来决定结果符号不就好了。

LinkList Multiply(LinkList L1,LinkList L2)//乘法实现,利用列竖式的思路
{
	LinkList p,q,temp_ele,temp_L3;
	LinkList L3=InitList();
	CreateList_Head(L3,0);
	int carry=0,product,i,t;
	q=L2->prior;
    t=0;
	while(q!=L2)
	{
		p=L1->prior;
		temp_ele=InitList();
		while(p!=L1)
		{
			product=p->data*q->data+carry;
			CreateList_Head(temp_ele,product%10);
			carry=product/10;
			p=p->prior;
		}
		if(carry)
		{
			CreateList_Head(temp_ele,carry);
			carry=0;
		}
		for(i=0;i<t;i++)
		{
			CreateList_Tail(temp_ele,0);//低位补0
		}
		temp_L3=L3;
		L3=Add(temp_ele,temp_L3);
		DestroyList(temp_L3);
		DestroyList(temp_ele);
		q=q->prior;
		t++;
	}
	if((L1->data==1&&L2->data==1)||(L1->data==0&&L2->data==0))//正数乘正数和负数乘负数结果为正
		L3->data=1;
	else//负数乘正数和正数乘负数结果为负
        L3->data=0;
	return L3;
}

        好了,现在我们便完美地实现两长整数的加、减、乘运算了。

        5、高位多余的0的消除

        细心的小伙伴可能会发现,在运算时可能会出现以下情况:

${1111-1111=0000}$

${1234-1111=0123}$

${-999 \times 0=-000}$

        虽然结果没有什么问题,但看起来还是不太舒服,那我们再写一个调整函数来处理高位多出来的0和0前面的负号即可。

void Adjust(LinkList L) //清除多余的0和0前面的负号
{
    LinkList p=L->next;
    while(p->data==0&&ListLength(L)>1)//删除高位的0
    {
        p->next->prior=L;
        L->next=p->next;
        free(p);
        p=L->next;
    }
	if(L->next->data==0&&ListLength(L)==1)//删除0前面的负号
		L->data=1;
}

        这样一来,计算也实现了,结果也没有问题了,我们便成功拿双向循环链表实现了高精度计算。

四、完整程序实现

        编写工具:VS Code(v1.84.2)

        编译器:GCC v8.1.0 (Built by MinGW-W64 project)

        测试工具2:Dev-C++ (v5.11)

        编译器:TDM-GCC 4.9.2 64-bit release

        以上两个平台均可正常运行。

        若要在Visual C++6.0实现只需将代码第106行的"long long"改为"_int64"即可,因为VC6.0中没有long long所以编译不通过。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAXSIZE 1000

typedef struct Node{//双向循环链表结点定义
	int data;
	struct Node *next;
	struct Node *prior;
}Node,*LinkList;

LinkList InitList()//初始化链表
{
	LinkList L;
	L=(Node *)malloc(sizeof(Node));
	L->data=1;
	L->next=L;
	L->prior=L;
	return L;
}

int ListLength(LinkList L)//求表长
{
    LinkList p;
    int t=0;
    p=L->next;
    while(p!=L)
    {
        t++;
        p=p->next;
    }
    return t;
}

void CreateList_Head(LinkList L,int num)//头插法建表
{
	LinkList p,n;
	p=L;
	n=(Node *)malloc(sizeof(Node));
	n->data=num;
	n->next=p->next;
	n->prior=p;
	p->next=n;
	n->next->prior=n;
}

void CreateList_Tail(LinkList L,int num)//尾插法建表
{
	LinkList p,n;
	p=L;
	n=(Node *)malloc(sizeof(Node));
	n->data=num;
	n->next=p;
	n->prior=p->prior;
	p->prior->next=n;
	p->prior=n;
}

void DestroyList(LinkList L)//销毁表
{
	LinkList p,q;
	p=L->next;
	while(p!=L)
	{
		q=p->next;
		free(p);
		p=q;
	}
	free(L);
}

void PrintList(LinkList L) //输出链表
{
	LinkList p;
	if(!L->data)		//负数前的负号
		printf("-");
	p=L->next;
    while(p!=L)
    {
        printf("%d",p->data);
        p=p->next;
    }
	printf("\n");
}

void Adjust(LinkList L) //清除多余的0和0前面的负号
{
    LinkList p=L->next;
    while(p->data==0&&ListLength(L)>1)//删除高位的0
    {
        p->next->prior=L;
        L->next=p->next;
        free(p);
        p=L->next;
    }
	if(L->next->data==0&&ListLength(L)==1)//删除0前面的负号
		L->data=1;
}

void GetNum(LinkList L1,LinkList L2)//将输入的数字(字符串)转换成链表
{
	char num[MAXSIZE];
	printf("输入操作数1:");
	gets(num);
	long long i;
	for(i=0;i<strlen(num);i++)
	{
		if((num[i]>='0'&&num[i]<='9')||num[i]=='-')	//判断输入是否合法
		{
			if(num[i]=='-')			//操作数为负数情况,负号不读入链表
			{
				L1->data=0;			//链表头结点数据域表示正负(1为正,0为负)
				continue;
			}
			CreateList_Tail(L1,num[i]-'0'); //采用尾插法(高位在前)
		}
		else
		{
			printf("输入错误!\n");
			exit(0);
		}
		
	}
	printf("输入操作数2:");
	gets(num);
	for(i=0;i<strlen(num);i++)
	{
		if((num[i]>='0'&&num[i]<='9')||num[i]=='-')	//判断输入是否合法
		{
			if(num[i]=='-')			//操作数为负数情况,负号不读入链表
			{
				L2->data=0;			//链表头结点数据域表示正负(1为正,0为负)
				continue;
			}
			CreateList_Tail(L2,num[i]-'0'); //采用尾插法(高位在前)
		}
		else
		{
			printf("输入错误!\n");
			exit(0);
		}
	}
}

void PrintResult(LinkList add_result,LinkList min_result,LinkList mul_result)//打印结果
{
	printf("两数之和为: ");
	PrintList(add_result);
	printf("两数之差为: ");
	PrintList(min_result);
	printf("两数之积为: ");
	PrintList(mul_result);
}

LinkList Add(LinkList L1,LinkList L2)//加法实现
{
    LinkList Minus(LinkList,LinkList);
	LinkList p=L1->prior,q=L2->prior;//从链表尾端(低位)开始
	LinkList L3;
	int carry=0,sum;
	if((L1->data==1&&L2->data==1)||(L1->data==0&&L2->data==0))//当两数均为或均为负时直接相加
	{
		L3=InitList();
		while(p!=L1&&q!=L2)
		{
			sum=p->data+q->data+carry;
			CreateList_Head(L3,sum%10);  //采用头插法(保证高位在前)
			carry=sum/10;
			p=p->prior;
			q=q->prior;
		}
		while(p!=L1)
		{
			sum=p->data+carry;
			CreateList_Head(L3,sum%10);
			carry=sum/10;
			p=p->prior;
		}
		while(q!=L2)
		{
			sum=q->data+carry;
			CreateList_Head(L3,sum%10);
			carry=sum/10;
			q=q->prior;
		}
		if(carry)
		{
			CreateList_Head(L3,carry);
		}
	}
	else
	{
		if(L1->data==1&&L2->data==0)//正数加负数,转换为正数减正数
		{
			L2->data=1;     //先将其置为正数,防止减法运算时负号判断问题
			L3=Minus(L1,L2);
			L2->data=0;		//恢复原来的符号
		}
		else//负数加正数,转换为第二个数减第一个数
		{
			L1->data=1;
			L3=Minus(L2,L1);
			L1->data=0;
		}
	}
	if(L1->data==0&&L2->data==0)//两负数之和仍为负数
		L3->data=0;
	Adjust(L3);		//清除多余的0和0前面的负号
	return L3;
}

int ValueCompare(LinkList L1,LinkList L2)//数值(绝对值)比较函数,返回1为L1表示的值大于L2,返回0为L2表示的值大于L1
{
	if(ListLength(L1)>ListLength(L2))
		return 1;
	else
	{
		if(ListLength(L1)<ListLength(L2))
			return 0;
		else
		{
			LinkList p,q;
			p=L1->next;
			q=L2->next;
			while(p!=L1&&q!=L2)
			{
				if(p->data>q->data)
					return 1;
				else
                {
					if(p->data<q->data)
						return 0;
					else
                    {
                        p=p->next;
                        q=q->next;
                    }
                }
			}
		}
	}
	return 1;
}

LinkList Minus(LinkList L1,LinkList L2)//减法实现,
{
	LinkList p,q,p_head,q_head;
	LinkList L3;
	int borrow=0,difference,Positive;
	if(L1->data==1&&L2->data==1)//正数减正数的情况,正常处理(a-b)
	{
		Positive=ValueCompare(L1,L2);//判断步骤确定结果的正负
		if(Positive)
		{
			p=L1->prior;
			q=L2->prior;
			p_head=L1;
			q_head=L2;
		}
		else
		{
			p=L2->prior;
			q=L1->prior;
			p_head=L2;
			q_head=L1;
		}
		L3=InitList();
		while(p!=p_head&&q!=q_head)
		{
			difference=p->data-borrow-q->data;
			if(difference<0)
				borrow=1;
			else
				borrow=0;
			CreateList_Head(L3,difference+10*borrow);
			p=p->prior;
			q=q->prior;
		}
		while(p!=p_head)	//已经进行了大小比较,q所指链表一定先比p所指链表结束
		{
			if(!borrow)
			{
				CreateList_Head(L3,p->data);
				p=p->prior;
			}
			else
			{
				difference=p->data-borrow;
				if(difference<0)
					borrow=1;
				else
					borrow=0;
				CreateList_Head(L3,difference+10*borrow);
				p=p->prior;
			}
		}
		if(!Positive)
			L3->data=0;
	}
	else
	{
		if(L1->data==1&&L2->data==0)//正数减负数的情况,将其转换为正数加正数(a-(-b))=a+b
		{
			L2->data=1;
			L3=Add(L1,L2);
			L2->data=0;
		}
		else
			if(L1->data==0&&L2->data==1)//负数减正数的情况,将其转换为负的正数加正数-a-b=-(a+b)
			{
				L1->data=1;
				L3=Add(L1,L2);
				L1->data=0;
				L3->data=0;
			}
			else//负数减负数的情况,将其转换为第二个数(正)减第一个数(正)-a-(-b)=b-a
			{
				L1->data=1;
				L2->data=1;
				L3=Minus(L2,L1);
				L1->data=0;
				L2->data=0;
			}
	}
    Adjust(L3);		//清除多余的0和0前面的负号
	return L3;
}

LinkList Multiply(LinkList L1,LinkList L2)//乘法实现,利用列竖式的思路
{
	LinkList p,q,temp_ele,temp_L3;
	LinkList L3=InitList();
	CreateList_Head(L3,0);
	int carry=0,product,i,t;
	q=L2->prior;
    t=0;
	while(q!=L2)
	{
		p=L1->prior;
		temp_ele=InitList();
		while(p!=L1)
		{
			product=p->data*q->data+carry;
			CreateList_Head(temp_ele,product%10);
			carry=product/10;
			p=p->prior;
		}
		if(carry)
		{
			CreateList_Head(temp_ele,carry);
			carry=0;
		}
		for(i=0;i<t;i++)
		{
			CreateList_Tail(temp_ele,0);//低位补0
		}
		temp_L3=L3;
		L3=Add(temp_ele,temp_L3);
		DestroyList(temp_L3);
		DestroyList(temp_ele);
		q=q->prior;
		t++;
	}
	if((L1->data==1&&L2->data==1)||(L1->data==0&&L2->data==0))//正数乘正数和负数乘负数结果为正
		L3->data=1;
	else//负数乘正数和正数乘负数结果为负
		L3->data=0;
    Adjust(L3);
	return L3;
}

int main()
{
	LinkList L1,L2,AddResult,MinusResult,MultiplyResult;
	L1=InitList();
	L2=InitList();
	GetNum(L1,L2);
    AddResult=Add(L1,L2);
    MinusResult=Minus(L1,L2);
	MultiplyResult=Multiply(L1,L2);
	PrintResult(AddResult,MinusResult,MultiplyResult);   
	return 0;
}
  • 47
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Nix Lockhart

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值