上周五去头条面试的时候,面试官给了道大数相加的题,觉得还是有些意思的,不敢私藏,拿出来给大伙品鉴品鉴。
1.问题
科学计算中往往需要很大的数字,我们可以使用如下的单链表结构来表示大数(整型),每个节点存储0-9的整数
class Node{
Node next;
int value;
}
如: 5274 可以表示为 5->2->7->4
问:如何实现大数的运算,比如大数相加(结果也用单链表表示)?
2.思考
我们知道:
加法运算是需要从低位到高位依次相加进位来得到最终的结果。即从个位开始,从右往左依次进行求和运算,若该位置的和大于等于10则向前进位。
那么便需要在运算时从链表的尾节点开始计算,然而在此处单链表只能从前往后进行操作,能不能想到一种手段,能够将操作链表的顺序给反转过来?
3.需求实现
为了方便链表创建和打印,在Node中定义了创建链表和打印链表的方法。
public class Node {
Node next;
int value;
/**
* 创建链表
* @param arr
* @return
*/
public static Node create(int[] arr) {
if(arr.length == 0) {
return null;
}
Node head = new Node();
head.value = arr[0];
Node current = head;
for (int i = 1; i < arr.length; i++) {
Node node = new Node();
node.value = arr[i];
current.next = node;
current = current.next;
}
return head;
}
/**
* 打印链表
* @param head
*/
public static void print(Node head) {
StringBuilder builder = new StringBuilder();
while (head != null) {
builder.append(head.value);
head = head.next;
}
System.out.println(builder.toString());
}
}
准备就绪我们就来看看怎么实现吧~
3.1 栈实现
分析:
既然需要将操作顺序颠倒过来,很自然地我就想到了借助栈这种先进后出的存储结构实现操作顺序的反转。
实现思路:
1.先将需要相加的两个链表分别入栈
2.将两个栈中内容依次出栈求和,计算进位值并将进位余下的值入结果栈
3.结果栈出栈生成单链表
实现代码:
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* 大数相加(利用整型单链表表示大数,再进行加法计算) 链表l1 2->6->7->3 表示2673 链表l2 3->8->7->4->2 表示38742
* 要求实现单链表加法,其结果也用单链表表示
*
* 分析:加法需要从最末位开始计算,相加的和大于10则向前进位 但此单链表方向无法满足进位规则,重点为实现其末位进位!
*
* 思路1:借助栈实现
*
* @author stephenshen
*
*/
public class AddTwoNodesByStack {
/**
* 链表相加
*/
public static Node add(Node head1, Node head2) {
//1.链表入栈
Stack<Integer> head1Stack = nodeToStack(head1);
Stack<Integer> head2Stack = nodeToStack(head2);
//2.栈相加
Stack<Integer> resultStack = stackAdd(head1Stack,head2Stack);
//3.链表出栈
Node resultNode = stackToNode(resultStack);
return resultNode;
}
/**
* 栈相加
* @param s1
* @param s2
* @return
*/
private static Stack<Integer> stackAdd(Stack<Integer> s1, Stack<Integer> s2){
Stack<Integer> resultStack = new Stack<>();
int sum = 0; // 和
int remainder = 0; // 进位值
while (!s1.isEmpty() || !s2.isEmpty()) {
sum = remainder;
if(!s1.isEmpty())
sum += s1.pop();
if(!s2.isEmpty())
sum += s2.pop();
remainder = sum / 10;
// 结果逐位入栈
resultStack.push(sum % 10);
}
if (remainder > 0) {
resultStack.push(remainder);
}
return resultStack;
}
/**
* 链表入栈
* @param head
* @return
*/
private static Stack<Integer> nodeToStack(Node head) {
Stack<Integer> stack = new Stack<>();
// 链表1入栈
while (head != null) {
stack.push(head.value);
head = head.next;
}
return stack;
}
/**
* 链表出栈
* @param stack
* @return
*/
private static Node stackToNode(Stack<Integer> stack) {
Node head = null;
Node current = null;
while (!stack.isEmpty()) {
Node node = new Node();
node.value = stack.pop();
if (head == null) {
head = node;
current = head;
} else {
current.next = node;
current = current.next;
}
}
return head;
}
}
3.2 链表反转实现
分析:
说到反转操作顺序,还能有比反转链表更直接的吗?
实现思路:
1.将原始的两个链表反转
2.将反转后的链表正序相加求和进位
3.将相加得到的链表再次反转得到最终链表
实现代码:
/**
* 大数相加(利用整型单链表表示大数,再进行加法计算) 链表l1 2->6->7->3 表示2673 链表l2 3->8->7->4->2 表示38742
* 要求实现单链表加法,其结果也用单链表表示
*
* 分析:加法需要从最末位开始计算,相加的和大于10则向前进位 但此单链表方向无法满足进位规则,重点为实现其末位进位!
*
* 思路2:反转链表实现
*
* @author stephenshen
*
*/
public class AddTwoNodesByInversion {
/**
* 正序链表相加
* @param h1
* @param h2
* @return
*/
public static Node add(Node h1, Node h2) {
//1.反转链表
h1 = reverse(h1);
h2 = reverse(h2);
//2.正序相加
Node node = reverseAdd(h1, h2);
//3.反转结果链表
return reverse(node);
}
/**
* 倒序链表相加
* @param h1
* @param h2
* @return
*/
public static Node reverseAdd(Node h1, Node h2) {
Node head = null; //头结点
Node current = null; //当前节点
int sum = 0; //和
int remainder = 0; //进位的值
while(h1 != null || h2 != null) {
sum = remainder;
if(h1 != null) {
sum += h1.value;
h1 = h1.next;
}
if(h2 != null) {
sum += h2.value;
h2 = h2.next;
}
remainder = sum / 10;
Node node = new Node();
node.value = sum % 10;
if(head == null) {
head = node;
}else {
current.next = node;
}
current = node;
}
if(remainder > 0) {
Node node = new Node();
node.value = remainder;
current.next = node;
}
return head;
}
/**
* 反转链表 循环实现,不改变原链表
*
* @param node
* @return
*/
private static Node reverse(Node node) {
Node head = null;
if(node == null || node.next == null)
return head;
while(node != null) {
Node tmp = new Node();
tmp.value = node.value;
if(head == null) {
head = tmp;
}else {
tmp.next = head;
head = tmp;
}
node = node.next;
}
return head;
3.3 逐层扫描实现
分析:
1.栈底层实现就是数组,栈实现中随着元素数量的增加栈底层的数组需要频繁扩容,能不能直接使用数组呢?
答:完全可以,最初确定数组大致长度能有效减少数组扩容次数
2.单链表的加法运算步骤可不可以拆分呢?
答:可以拆分为相加、进位、再次转化为链表三个步骤
3.可拆的话每一步都需要逆序操作吗?
答:只有进位是需要从右往左即逆序进位
实现思路:
1.对应数位顺序逐位相加
2.逆序进位生成结果数组
3.结果数组顺序转化为链表
实现代码:
import java.util.Arrays;
/**
* 大数相加(利用整型单链表表示大数,再进行加法计算) 链表l1 2->6->7->3 表示2673 链表l2 3->8->7->4->2 表示38742
* 要求实现单链表加法,其结果也用单链表表示
*
* 分析:加法需要从最末位开始计算,相加的和大于10则向前进位 但此单链表方向无法满足进位规则,重点为实现其末位进位!
*
* 思路3:分步扫描实现
*
* @author stephenshen
*
*/
public class AddTwoNodesByScanning {
/**
* 链表加法
*
* @param h1
* @param h2
* @return
*/
public static Node add(Node h1, Node h2) {
// 1.逐位相加
int[] arr = addOneByOne(h1, h2);
// 2.逆序进位
reverseCarry(arr);
// 3.顺序转化为链表
return createNode(arr);
}
/**
* 获取链表长度
* @param node
* @return
*/
private static int getLength(Node node) {
int length = 0;
while (node != null) {
length++;
node = node.next;
}
return length;
}
/**
* 逐位相加
* @param h1
* @param h2
* @return
*/
private static int[] addOneByOne(Node h1, Node h2) {
// 1.按链表最大长度+1创建数组
int len1 = getLength(h1);
int len2 = getLength(h2);
int length = Math.max(len1, len2) + 1;
int[] arr = new int[length];
int index = 1; //下标从1开始,0位置留待最高位的和 大于10时 进位使用
int start1 = length - len1; //链表1头结点对应位置
int start2 = length - len2; //链表2头结点对应位置
for( ;index < length; index ++) {
if(index >= start1) {
arr[index] += h1.value;
h1 = h1.next;
}
if(index >= start2) {
arr[index] += h2.value;
h2 = h2.next;
}
}
return arr;
}
/**
* 逆序进位
* @param arr
*/
private static void reverseCarry(int[] arr) {
int remainder = 0; //进位值
for (int i = arr.length - 1; i >= 0; i--) {
//补上前一位的进位值
arr[i] += remainder;
//计算新的进位值
remainder = arr[i] / 10;
//计算新的当前值
arr[i] = arr[i] % 10;
}
}
/**
* 顺序转化为链表
* @param arr
* @return
*/
private static Node createNode(int[] arr) {
int start = 0;
//统计数组有效长度
for (int i = 0; i < arr.length; i++) {
if(arr[i] == 0) {
start++;
}else {
break;
}
}
//转化为有效数组
int[] arr1 = Arrays.copyOfRange(arr, start, arr.length);
//转化为链表
return Node.create(arr1);
}
}
测试类:
public class Test {
public static void main(String[] args) {
int[] arr1 = { 2, 6, 7, 3 };
Node head1 = Node.create(arr1);
System.out.print("[原始链表A]");
Node.print(head1);
int[] arr2 = { 3, 9, 7, 4, 2 };
Node head2 = Node.create(arr2);
System.out.print("[原始链表B]");
Node.print(head2);
System.out.println("--大数链表相加实现--");
Node result1 = AddTwoNodesByInversion.add(head1, head2);
System.out.print("链表反转实现:");
Node.print(result1);
Node result2 = AddTwoNodesByStack.add(head1, head2);
System.out.print("栈实现:");
Node.print(result2);
Node result3 = AddTwoNodesByScanning.add(head1, head2);
System.out.print("逐层扫描实现:");
Node.print(result3);
}
}
附上测试结果:
4.题外话
1.为什么不用递归?
答:大数可能很大,这种情况下需要规避递归导致的内存溢出。
2.反转实现时为什么不在原来的链表上进行反转?
答:一个大数可能在很多个地方都需要用来计算,故尽量不去修改原来的链表。
3.哪种实现方式更好?
答:根据自己需要进行选择
-栈实现
容易理解和实现,可以很方便地类推出大数的减法和乘除法
时间复杂度和空间复杂度最高,
-链表反转实现
代码最少,适用于大数的加减法,乘除法不适用
时间复杂度和空间复杂度较高
-逐层扫描实现
比较容易理解,适用于大数的加减乘除,当类推到乘除法时不如栈实现简单清晰,需要对数组长度严格控制
时间复杂度和空间复杂度最低