[剑指Offer] 第3章课后题详解
目录
大数加法
本题为《剑指Offer》“面试题12:打印1到最大的n位数”一节中的“相关题目”。
定义一个函数,在该函数中可以实现任意两个整数的加法。
分析
由于没有限定输入两个数的大小范围,所以需要把它当做大数问题来处理。大数无法用int,long甚至long long类型来表示,这里选择用字符串来表示。这里需要注意的一个点是,没有限制不能输入负数,还得考虑负数的情况。由于负数的加入,我们需要考虑,运算到底是做加法还是做减法,最后结果的符号问题。
解法
bigNumberPlus函数为所求函数,其余4个函数分别完成判断加减法,比较绝对值,计算,输出结果的任务。本解法采用的是顺序容器string([C++ 面试基础知识总结] 顺序容器)。
#include <iostream>
using namespace std;
bool flag1,flag2,flag3;
auto computeType(string &s1, string &s2) -> bool{
flag1 = s1[0] == '-';
flag2 = s2[0] == '-';
if (flag1){
s1.erase(s1.begin());
}
if (flag2){
s2.erase(s2.begin());
}
return flag1 ^ flag2;
}
auto judge(const string &s1, const string &s2) -> bool{
if(s1.size() != s2.size()){
return s1.size() > s2.size();
}
auto b1 = s1.begin(), b2 = s2.begin();
while(b1 != s1.end() && b2 != s2.end()){
if (*b1 != *b2){
return *b1 > *b2;
}
++b1;
++b2;
}
return true;
}
auto compute(const string &s1, const string &s2, bool type) -> string{
string sum(s1.size() + 2,'0');
bool takeOver = 0;
auto p1 = s1.rbegin(), p2 = s2.rbegin();
auto p3 = sum.rbegin();
while(p2 != s2.rend()){
if (*p1 < '0' || *p1 > '9'|| *p2 < '0' || *p2 > '9')
{
throw new runtime_error("invalid");
}
int v1 = *p1++ - '0', v2 = *p2++ - '0', result;
if (type){
if(v1 >= v2 + takeOver){
result = v1 - v2 - takeOver;
*p3++ = char('0' + result % 10);
takeOver = 0;
}
else{
result = v1 + 10 - v2 - takeOver;
*p3++ = char('0' + result % 10);
takeOver = 1;
}
}
else{
result = v1 + v2 + takeOver;
*p3++ = char('0' + result % 10);
takeOver = result / 10;
}
}
while(p1 != s1.rend()){
if (*p1 < '0' || *p1 > '9')
{
throw new runtime_error("invalid");
}
int v1 = *p1++ - '0', result;
if (type){
if(v1 >= takeOver){
result = v1 - takeOver;
*p3++ = char('0' + result % 10);
takeOver = 0;
}
else{
result = v1 + 10 - takeOver;
*p3++ = char('0' + result % 10);
takeOver = 1;
}
}
else{
result = v1 + takeOver;
*p3++ = char('0' + result % 10);
takeOver = result / 10;
}
}
if(takeOver == 1){
*p3++ = char('0' + takeOver);
}
if(flag3){
*p3++ = '-';
}
返回运算结果字符串
return sum;
}
auto printResult(const string &s) -> void{
bool isBegin0 = true;
for(auto begin = s.begin(), end = s.end(); begin != end; ++begin){
if (isBegin0 && *begin != '0'){
isBegin0 = false;
}
if (!isBegin0){
cout << *begin;
}
}
cout << endl;
}
auto bigNumberPlus(const string &s1, const string &s2) -> void{
string realS1(s1), realS2(s2);
bool type = computeType(realS1, realS2);
flag3 = judge(realS1, realS2) ? flag1 : flag2;
printResult(judge(s1, s2) ? compute(realS1, realS2, type) : compute(realS2, realS1, type));
}
int main(int argc, const char * argv[]) {
string s1,s2;
cin >> s1 >> s2;
bigNumberPlus(s1, s2);
return 0;
}
优化
本解法还有两个地方可以进一步优化:
1.在computeType函数中,对string类型容器进行删除首元素操作,这是不推荐的,可以考虑用deque<char>
或者list<char>
来替换string。原因详见[C++ 面试基础知识总结] 顺序容器。
2.在compute函数中,做了两步运算,第一步为s1低位与s2的运算,第二步是s1高位与s2最高位进位或退位的运算,这里可以再提取一个函数出来,用于计算两个相等长度字符串的运算,接收4个参数,分别为2个加数的引用、进位标志和运算类型,返回最高位进位标志,然后调用两次,第一次是s1低位与s2的运算,第二次是s1高位与全为’0’的字符串的运算。这样可以让代码更简洁,提升代码的拓展性。
链表的中间节点
本题为《剑指Offer》“面试题15:链表中倒数第k个结点”一节中的“相关题目”。
求链表的中间结点,如果链表中的结点总数为奇数,返回中间结点;如果结点总数为偶数,返回中间两个结点的任意一个。链表节点定义如下:
struct ListNode{
int m_nValue;
ListNode* m_pNext;
};
分析
可以定义两个指针,一个每次走两步,一个每次走一步,走两步的指针抵达链表尾部时,则走一步的指针正好在链表中间。需要注意的是空链表的情况,且走两步的指针不能连续走两步,每走一步都必须判定是否已达到链表尾部。
解法
auto findMidNode(ListNode* pListHead) -> ListNode*{
if(pListHead == NULL){
return NULL;
}
ListNode* pAhead = pListHead;
ListNode* pBehind = pListHead;
while(pAhead -> m_pNext != NULL){
pAhead = pAhead -> m_pNext;
pBehind = pBehind -> m_pNext;
if(pAhead -> m_pNext == NULL){
return pBehind;
}
pAhead = pAhead -> m_pNext;
}
return pBehind;
}
环形链表
本题为《剑指Offer》“面试题15:链表中倒数第k个结点”一节中的“相关题目”。
判断一个单向链表是否形成了环形结构。链表节点定义如下:
struct ListNode{
int m_nValue;
ListNode* m_pNext;
};
分析
与上道题类似,定义两个指针,走两步的指针如果能追到走一步的指针,则形成了环形结构,如果走到底了,则没有形成环形结构。同样注意的也是空链表的处理和走两步之间的判定。
解法
auto hasCircle(ListNode* pListHead) -> bool{
if(pListHead == NULL){
return NULL;
}
ListNode* pAhead = pListHead;
ListNode* pBehind = pListHead;
while(pAhead -> m_pNext != NULL){
pAhead = pAhead -> m_pNext;
pBehind = pBehind -> m_pNext;
if(pAhead -> m_pNext == NULL){
return false;
}
pAhead = pAhead -> m_pNext;
if(pAhead == pBehind){
return true;
}
}
return false;
}
反转链表
本题为《剑指Offer》“面试题16:反转链表”一节中的“本题拓展”。
定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点,用递归的方式实现。链表节点定义如下:
struct ListNode{
int m_nKey;
ListNode* m_pNext;
};
分析
数据结构中链表插入有两种方法,头插法和尾插法,尾插法是链表尾部最后一个节点的next指针指向插入节点,头插法是用插入节点的next指针指向链表的第一个节点(非固定头节点的情况)。将链表中的节点依次用头插法的方式插入到一个新的空链表中则可以实现链表的反转。这里需要注意的是输入链表为空,输入链表只有一个节点的特例,以及链表断裂的防止。
解法
auto reverseList(ListNode* pListHead) -> ListNode*{
if(pListHead == NULL){
return NULL;
}
if(pListHead -> m_pNext == NULL){
return pListHead;
}
ListNode* nextNode = pListHead -> m_pNext;
pListHead -> m_pNext = NULL;
return insertHead(nextNode, pListHead);
}
auto insertHead(ListNode* pListHead, ListNode* newListHead) -> ListNode*{
if(pListHead -> m_pNext == NULL){
pListHead -> m_pNext = newListHead;
return pListHead;
}
ListNode* nextNode = pListHead -> m_pNext;
pListHead -> m_pNext = newListHead;
return insertHead(nextNode, pListHead);
}