《剑指offer》第三章 16-26题解

第三章 高质量代码

1.代码的完整性

剑指 Offer 16. 数值的整数次方

实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。

输入:x = 2.00000, n = 10
输出:1024.00000

输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25
  • 初始解法:x*x重复n-1次。但这种情况下没有考虑n为负数和零。
  • 完整解法:应该对负数进行-操作,对于零应该抛出异常。但这种方法需要在循环中n-1次乘法。
  • 优化:考虑利用递归可以求出x的n次方,即先求出xn/2,而xn/2可以由xn/4求出。即如果n是奇数,相当于多乘一次本身,而偶数可以直接递归平方求出。
    • **利用右移运算代替/2运算,用位与运算代替求余运算判断奇偶性。**位运算的效率比乘除以及求余效率要高。

考验思维的全面性

    double myPow(double x, int n) {
        if(x == 1|| n == 0) return 1;
        if(n == 1) return x;

        long exp = (long)n;
        if(exp > 0){
            double res = myPow(x, exp>>1);
             res *= res;
             if(exp&1) res *= x;
             return res;
        }
        if(n < 0){
            double res = myPow(x, (-exp)>>1);
            res *= res;
            if(exp&1) res *= x;
            return 1/res;
        }
        return 0;
    }

剑指 Offer 17. 打印从1到最大的n位数

输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。

输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]

方法一:用字符串模拟数字加法。

首先要考虑到当n很大的时候(比如100),打印出来的数很有可能是超过了INT_MAX的范围的,所以我们用字符串来表示每个数。

当然,在这一题中,由于返回的是一个 int 型的数组,所以是不可能超过INT_MAX的,但是一般大数问题都不会要求返回 int 数组来保存每一位数,而是循环输出每一位数。

我们的思路就是,假设 n = 3,就定义一个字符串,初始化为 “000”,然后用它来循环模拟从1到最大的n位数,并循环保存到 int 数组中(在真实情况下则是循环输出)。

意识到是大数问题。

利用在加1时第一个字符产生了进位,则已经是最大的n位。

按照阅读习惯,字符串“00980”需要输出为980而不是00980。

    vector<int> output;
    vector<int> printNumbers(int n) {
        if(n <= 0) return vector<int>(0);
        string s(n,'0');//用字符串来表示大数,例如n=3
        while( !increment(s) ) outputNumbers(s);//increment实现字符串上+1,如果返回true,表示已经到达最大值999
        return output;
    }

    bool increment(string & s){//模拟数字的累加过程,并判断是否越界(即 999 + 1 = 1000,就是越界情况
        bool isOverFlow = false;//默认情况下没有到达最大值
        int carry = 0;//carry表示进位
        for(int i = s.size()-1; i >= 0; --i){
            int cur = s[i] - '0' + carry;//cur表示当前这次操作
            if(i == s.size()-1) ++cur;//如果此时i在个位,cur进行+1,相当于对字符串s中表示的数字进行累加过程
            if(cur >= 10){//如果当前操作cur大于10,表示可能要进行进位
                if(i == 0) isOverFlow = true;//假如已经在最大位,而current++之后>=10,说明循环到头了,即999 + 1 = 1000
                else{//只是普通进位
                    carry = 1;
                    s[i] = cur - 10 + '0';//去除进位10以后,转换为字符的形式
                }
            }else{//如果没有进位,更新s[i]的值,然后跳出循环,表示本次累加结束,去将本次累加结果存到输出,等待下次累加操作
                    s[i] = cur + '0';
                    break;
            }
        }
        return isOverFlow;
    }
    
   void outputNumbers(string s){
        // 本函数用于循环往output中添加符合传统阅读习惯的元素。比如001,我们会添加1而不是001。
        bool isFirtNonZero = false;// 判断是否经过第一个非‘0’,比如0010前面的两个0
        string temp = "";
         for(int i=0; i<s.length(); ++i){
            if(!isFirtNonZero && s[i] != '0') isFirtNonZero = true;
            if(isFirtNonZero) temp += s[i];//如果已经经过第一个非零字符,后面的‘0’需要保留
        }
        output.push_back(stoi(temp));
    }

方法二:递归全排列解法。

扫描二维码关注公众号,回复: 16472710 查看本文章

假设 n = 3,要输出的数其实就是三位数的全排列(000,001,002,…,999,当然 000 不能输出),我们用递归来表示出这个过程即可。注意000,以及前缀0不能打印出来。

没想到stoi这个函数这么强大,stoi(str);

如果 s = “000”,则temp会是空,那么不进行输出;而且stoi会自动把“009”这类字符串转化成数字9,自动把前缀0去掉。

不过在实际面试中,大概率最后要求输出的是string,就不能使用stoi函数了,那么最后还是需要借助方法1的outputNumbers函数,去掉前缀0,只不过为了防止输出“000”,需要把37行修改成if(temp != "") output.push_back(stoi(temp));

   vector<int> printNumbers(int n) {
        if(n <= 0) return vector<int>(0);
        vector<int> output;
        string s(n,'0');
        allArrange(s, output, 0);
        return output;
    }
    void allArrange(string &s, vector<int> &output, int pos){
        if(pos == s.size()){//递归停止条件是已经修改了字符串的最后一位
            int temp = stoi(s);//如果 s = "000",则temp会是空,那么不进行输出;而且stoi会自动把“009”这类字符串转化成数字9,自动把前缀0去掉
            if(temp) output.push_back(temp);
            return;
        }
        for(int i = 0; i < 10; ++i){
            s[pos] = i + '0';
            allArrange(s, output, pos + 1);
        }
    }

剑指 Offer 18. 删除链表的节点

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。

输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.

从头结点开始顺序查找,通过检查head->next->val == val来找到待删除结点的前一个结点,head ->next = head->next->next

剑指offer和此题有区别:剑指上面直接给出了待删的结点ListNode,所以它和它的下一个节点可以直接找到,不需要从头遍历,直接把该节点的下一个节点值复制到待删结点,再把下一个结点删除,达到了删除这个结点的效果。但如果next为空是尾节点,就需要从头遍历,把待删结点的前一个结点next指向空。

    ListNode* deleteNode(ListNode* head, int val) {
        if(!head) return nullptr;
        if(head->val == val) return head->next;//如果要删除的点是头结,直接令head指向next,这样如果链表只有一个节点,则为空,如果不是一个节点,则相当于把头结点删掉。
        ListNode* dummy = head;//dummy保存头结点
        while(head->next){//这样找到的head是待删除结点的前一个结点
            if(head->next->val == val){
                head->next = head->next->next;
                break;
            }else{
                head = head->next;
            }
        }
        return dummy;
    }

剑指 Offer 20. 表示数值的字符串

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。

数值(按顺序)可以分成以下几个部分:

  1. 若干空格
  2. 一个 小数 或者 整数
  3. (可选)一个 ‘e’ 或 ‘E’ ,后面跟着一个 整数
  4. 若干空格

小数(按顺序)可以分成以下几个部分:

  1. (可选)一个符号字符(’+’ 或 ‘-’)
  2. 下述格式之一:
    1. 至少一位数字,后面跟着一个点 ‘.’
    2. 至少一位数字,后面跟着一个点 ‘.’ ,后面再跟着至少一位数字
    3. 一个点 ‘.’ ,后面跟着至少一位数字

整数(按顺序)可以分成以下几个部分:

  1. (可选)一个符号字符(’+’ 或 ‘-’)
  2. 至少一位数字
//状态机
bool isNumber(string s) {
        if(s.empty()) return false;
        int n = s.size();
        int index = -1;
        bool hasDot = false,hasE = false, hasOp = false,hasNum = false;

        while(index < n && s[++index] == ' ');//把字符串前面的空格去掉
        while(index < n){
            if('0' <= s[index] && s[index] <= '9') hasNum = true;
            else if(s[index] == 'E' ||  s[index] == 'e'){
                if(hasE || !hasNum) return false;//如果已经有E了,或者前面没有数字,则直接返回false
                hasE = true;
                hasOp = false; hasDot = false; hasNum = false;//后面就要重置,因为e后面这些都可以跟
            }else if(s[index] == '+' || s[index] == '-'){
                if(hasOp || hasNum || hasDot) return false;//加号前面不能有加减号、数字(否则就是表达式)和小数点
                hasOp = true;
            }else if(s[index] == '.'){
                if(hasDot || hasE) return false;
                hasDot = true;
            }else if(s[index] == ' ') break;//有空格直接退出
            else return false;
            ++index;
        }
        while(index < n && s[++index] == ' ');//把后面得空格去掉
        return hasNum && (index == n);
    }

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。

输入:nums = [1,2,3,4]
输出:[1,3,2,4] 
注:[3,1,2,4] 也是正确的答案之一。
    vector<int> exchange(vector<int>& nums) {
        int n = nums.size();
        if(n <= 1) return nums;
        int left = 0,right = n-1;
        while(left < right){
            while( left < right && (nums[left]&1) == 1){//如果left是奇数,就一直右移,直到碰到偶数
                ++left;
            }
            while(left < right && (nums[right]&1) == 0){//如果right是偶数,就一直左移,直到碰到奇数
                --right;
            }
            if(left < right) swap(nums[left],nums[right]);//如果能换,就交换奇偶数的位置
        }
        return nums;
    }

这道题如果扩展成按照数组大小,能否被3整除等条件,分为前后两部分,为了使代码具有可扩展性。把整个函数解耦成两个部分:一是判断数字应该在数组前半部分还是后半部分的标准;二是拆分数组的操作。

2.代码的鲁棒性

剑指 Offer 22. 链表中倒数第k个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.

应注意代码鲁棒性:如果k的大小超过链表的长度,则返回空。

    ListNode* getKthFromEnd(ListNode* head, int k) {
        if(!head || k == 0) return nullptr;
        ListNode * slow = head,* fast = head;
        while(fast && k){
            --k;
            fast = fast->next;
        }
        if(k) return nullptr;//如果k的大小超过链表的长度,则返回空
        while(fast){
            fast = fast->next;
            slow = slow->next;
        }
        return slow;
    }

剑指 Offer 24. 反转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
/*非递归
    ListNode* reverseList(ListNode* head) {
        ListNode *pre = nullptr,*cur = nullptr;
        while(head){
            cur = head->next;//用cur保存修改当前节点之前的指向
            head->next = pre;//将当前head节点指向上一个节点
            pre = head;
            head = cur;
        }
        return pre;
    }*/
    //递归
    ListNode* reverseList(ListNode* head,ListNode *pre = nullptr) {
        if(!head) return pre;
        ListNode* cur = head->next;
        head->next = pre;
        return reverseList(cur,head);
    }

剑指 Offer 25. 合并两个排序的链表

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
/*原地合并,不开辟新节点
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if(!l1) return l2;
        if(!l2) return l1;
        if(l1->val <= l2->val){
            l1->next = mergeTwoLists(l1->next,l2);
            return l1;
        }else{
            l2->next = mergeTwoLists(l2->next,l1);
            return l2;
        }
    }*/
    //开辟新节点
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2){
        if(!l1) return l2;
        if(!l2) return l1;
        ListNode* mergeHead = nullptr;
        if(l1->val <= l2->val){
            mergeHead = l1;
            mergeHead->next = mergeTwoLists(l1->next,l2);
        }else{
            mergeHead = l2;
            mergeHead->next = mergeTwoLists(l2->next,l1);
        }
        return mergeHead;
    }

剑指 Offer 26. 树的子结构

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)。B是A的子结构, 即 A中有出现和B相同的结构和节点值。

例如:给定的树 A:
     3
    / \
   4   5
  / \
 1   2
给定的树 B:
   4 
  /
 1
 返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。

与二叉树相关的代码,在每次使用指针的时候,都首先问自己这个指针有没有可能是nullptr,如果是nullptr是怎么处理。

    bool isSubStructure(TreeNode* A, TreeNode* B) {//递归在A中寻找 与B根节点 相同的节点
        if(!A || !B) return false;//
        bool flag = false;
        if(A->val == B->val) flag = isStructure(A, B);
        if(flag) return true;
        else{
            return isSubStructure(A->left,B) || isSubStructure(A->right,B);
        }

    }
    bool isStructure(TreeNode* A, TreeNode* B){//判断A和B树的结构是否完全一样
        if(!B) return true;//如果B已经遍历结束,直接返回true
        if(!A || A->val != B->val) return false;
        return isStructure(A->left,B->left) && isStructure(A->right,B->right);//如果当前节点相同,就去看他们的左右子节点是否相同
    }

猜你喜欢

转载自blog.csdn.net/ClaireSy/article/details/117220150