【leetcode刷题之路】剑指Offer——字符串+链表+双指针

文章目录

      • 1 字符串
        • 1.1【字符串】【双指针】剑指 Offer 05 - 替换空格
        • 1.2【字符串】剑指 Offer 58 - II. 左旋转字符串
        • 1.3 【双指针】【字符串】剑指 Offer 20 - 表示数值的字符串
        • 1.4 【双指针】【字符串】剑指 Offer 67 - 把字符串转换成整数
      • 2 链表
        • 2.1 【回溯】【链表】剑指 Offer 06 - 从尾到头打印链表
        • 2.2 【双指针】【链表】剑指 Offer 24 - 反转链表
        • 2.3 【链表】【回溯】【哈希表】剑指 Offer 35 - 复杂链表的复制
      • 3 双指针
        • 3.1 【链表】【双指针】剑指 Offer 18 - 删除链表的节点
        • 3.2 【双指针】剑指 Offer 22 - 链表中倒数第k个节点
        • 3.3 【递归】剑指 Offer 25 - 合并两个排序的链表
        • 3.4 【双指针】剑指 Offer 52 - 两个链表的第一个公共节点
        • 3.5 【双指针】剑指 Offer 21 - 调整数组顺序使奇数位于偶数前面
        • 3.6 【双指针】剑指 Offer 57 - 和为s的两个数字
        • 3.7 【双指针】剑指 Offer 58 - I - 翻转单词顺序

1 字符串

1.1【字符串】【双指针】剑指 Offer 05 - 替换空格

https://leetcode.cn/problems/ti-huan-kong-ge-lcof/

  首先计算出字符串中的空格数量,然后对原字符串进行扩充,之后使用双指针i和j,i指向原字符串的尾部,j指向新字符串的尾部,然后双指针一起从尾部向前移动,如果此时i为空,则发生替换,否则直接挪到j的位置即可。

class Solution {
public:
    string replaceSpace(string s) {
        int length = s.size();
        int count = 0;
        for(int i=0;i<length;i++)
        {
            if(s[i]==' ') count++;
        }
        s.resize(length + 2 * count);
        for(int i=length-1,j=s.size()-1;i>0,j>i;i--,j--)
        {
            if(s[i]==' ')
            {
                s[j] = '0';
                s[j-1] = '2';
                s[j-2] = '%';
                j-=2;
            }
            else s[j] = s[i];
        }
        return s;
    }
};

1.2【字符串】剑指 Offer 58 - II. 左旋转字符串

https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/

(1)使用了额外的字符串来进行移动,左移n位可以看成右移length-n位,最后对length取余就是移动后的位置。
(2)直接在原字符串上修改,这里举一个例子来帮助大家理解:
  我们假设s=‘abcde’,n=2,那么最后的结果应该是cdeab,我们把这个结果进行翻转可以得到baedc,而这个字符串与原来的abcde的区别就是ab进行对称翻转+cde进行对称翻转,把他们分开的点就是第n个字符的位置,所以我们可以基于这个发现,先对前n个字符进行翻转,再对剩下的字符进行翻转,最后对整个字符串进行翻转。

//方法1---借助额外字符串
class Solution {
public:
    string reverseLeftWords(string s, int n) {
        string ans = s;
        int length = s.size();
        for(int i=0;i<length;i++)
        {
            ans[(i+length-n)%length] = s[i];
        }
        return ans;
    }
};

//方法2---在原字符串上修改
class Solution {
public:
    string reverseLeftWords(string s, int n) {
        //对前n个字符进行翻转
        reverse(s.begin(),s.begin()+n);
        //对剩下的字符进行翻转
        reverse(s.begin()+n,s.end());
        //对整个字符串进行翻转
        reverse(s.begin(),s.end());
        return s;
    }
};

1.3 【双指针】【字符串】剑指 Offer 20 - 表示数值的字符串

https://leetcode.cn/problems/biao-shi-shu-zhi-de-zi-fu-chuan-lcof/

首先通过双指针找到字符串中不为空的部分,对这部分内容进行数值判断

其实这道题的难点在于特殊符号±eE.的判断,如果是纯数字的组成一定是数值,下面对这些特殊符合分别进行分析

(1)小数点.

  • 如果这个数值含有小数点的话,那么有且只能有一个小数点,且小数点的前后必须都是数字;
  • 如果这个数值是科学计数法,那么小数点只能出现在e/E的前面,比如5.3e2;

(2)科学计数法e/E

  如果这个数值采用科学计数法的话,那么e/E有且只能出现一次,并且e/E的前后必须都是数字;

(3)符号±

  • 如果这个数值有符号的话,那么符号必须得在数值的第一位;
  • 如果这个数值是科学计数法,那么符号的前一位可以是e/E,但是后一位绝对不能是e/E,比如-1E-16;
class Solution {
public:
    bool isNumber(string s) {
        int i = 0 , j = s.size()-1;
        //找到字符串中第一个不为空的位置
        for(;i<s.size();i++)
        {
            if(s[i]!=' ') break;
        }
        //从末尾找到字符串最后一个不为空的位置
        for(;j>0;j--)
        {
            if(s[j]!=' ') break;
        }
        //判断是否为数值,以及是否有小数点和e/E
        bool num_flag = false;
        bool dot_flag = false;
        bool e_flag = false;
        for(int k=i;k<j+1;k++)
        {
            //判断是否为数字
            if(isdigit(s[k]))
            {
                num_flag = true;
            }
            //判断是否为小数点,并且之前是否出现过小数点和e/E
            else if(s[k]=='.' && !dot_flag && !e_flag)
            {
                dot_flag = true;
            }
            //判断是否为e/E,并且之前是否出现过e/E和数字
            else if((s[k]=='e' || s[k]=='E') && !e_flag && num_flag)
            {
                e_flag = true;
                //因为e/E的前后必须都是数字,所以如果找到了e/E就把num_flag设为false,
                //遇到下一个数字再设为true,避免出现12e的情况
                num_flag = false;
            }
            //判断是否为+-,并且符号是否在数值的首位,或者前一位是e/E
            else if((s[k]=='+' || s[k]=='-') && (k==i || s[k-1]=='e' || s[k-1]=='E'))
            {
                continue;
            }
            //其他均为非法情况,输出false
            else return false;
        }
        return num_flag;
    }
};

1.4 【双指针】【字符串】剑指 Offer 67 - 把字符串转换成整数

https://leetcode.cn/problems/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof/

这道题的思路其实不难想出来,就是找到数值的部分然后转化为整数,主要是需要考虑很多边界条件,这里列举我碰到的一些问题。
(1)只有一个字符,且该字符为空格或+或-,这种需要返回0;
(2)对于含有小数点的数值,比如3.1415,应该输出3,因为小数点在本题中属于多余字符;
(3)数值中间可能存在多余字符,比如123a456,应该输出123,注意识别其中的多余字符;
(4)开头有且只能有一个+或-,对于±1,应该输出0;
(5)注意int的边界条件,可能在字符串按序遍历*10的时候就已经超出int大小了,这里可以提前break循环。

class Solution {
public:
    int strToInt(string str) {
        //解题思路中第一种情况
        if(str.size()==1 && (str[0]==' ' || str[0]=='+' || str[0]=='-')) return 0;
        int i = 0 , j = str.size()-1;
        //找出数值部分的第一个元素
        for(;i<str.size();i++)
        {
            if(isdigit(str[i]) || str[i]=='+' || str[i]=='-') break;
            else if(str[i]==' ') continue;
            else return 0;
        }
        //找出数值部分的最后一个元素
        for(;j>0;j--)
        {
            if(isdigit(str[j])) break;
        }
        //判断数值的正负
        bool sym = true;
        if(str[i]=='+' || str[i]=='-')
        {
            if(str[i+1]=='+' || str[i+1]=='-') return 0;
            else
            {
                if(str[i]=='-') sym = false;
                i++;
            }
        }
        //判断是否超过int大小
        long long num = 0;
        bool overflow = false;
        while(i<=j)
        {
            if(!isdigit(str[i])) break;
            num = num * 10 + str[i] - '0';
            if(num!=int(num))
            {
                overflow = true;
                break;
            }
            i++; 
        }
        //根据符号正负选择对应的输出
        if(sym)
        {
            if(!overflow) return num;
            else return pow(2,31)-1;
        }
        else
        {
            if(!overflow) return -num;
            else return -pow(2,31);
        }
    }
};

2 链表

2.1 【回溯】【链表】剑指 Offer 06 - 从尾到头打印链表

https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/

(1)常规思路,先正序存入一个数组中,然后再逆序存入另一个数组中,作为最后结果;
(2)回溯法,先找到链表的最后一个元素,然后一步步往前输出到数组中。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
//常规思路
class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        vector<int> temp , ans;
        ListNode *p = head;
        while(p)
        {
            temp.push_back(p->val);
            p = p->next;
        }
        for(int i=temp.size()-1;i>=0;i--)
        {
            ans.push_back(temp[i]);
        }
        return ans;
    }
};

//回溯法
class Solution {
public:
    vector<int> ans;
    vector<int> reversePrint(ListNode* head) {
        if(!head) return ans;
        reversePrint(head->next);
        ans.push_back(head->val);
        return ans;
    }
};

2.2 【双指针】【链表】剑指 Offer 24 - 反转链表

https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/

  定义两个指针,cur指向头结点,ans指向NULL,在每次遍历中都让cur的下一个元素指向ans,实现反转。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode *cur = head , *ans = NULL;
        while(cur)
        {
            ListNode *tmp = cur->next;
            cur->next = ans;
            ans = cur;
            cur = tmp;
        }
        return ans;
    }
};

2.3 【链表】【回溯】【哈希表】剑指 Offer 35 - 复杂链表的复制

https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof

  这道题目考察的是链表的深拷贝,这里借助哈希表来存储结点,而且这个链表有一个随机指针,所以普通的复制是行不通的,因此采用了回溯的方法,每一次先看待复制的结点是否在哈希表中,如果在的话才好复制random指向的结点。

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/
class Solution {
public:
    unordered_map<Node*,Node*> hash_map;
    Node* copyRandomList(Node* head) {
        if(head == nullptr) return nullptr;
        if(!hash_map.count(head))
        {
            Node *new_node = new Node(head->val);
            hash_map[head] = new_node;
            new_node->next = copyRandomList(head->next);
            new_node->random = copyRandomList(head->random);
        }
        return hash_map[head];
    }
};

3 双指针

3.1 【链表】【双指针】剑指 Offer 18 - 删除链表的节点

https://leetcode.cn/problems/shan-chu-lian-biao-de-jie-dian-lcof/

  采用双指针,cur指向当前遍历的结点,pre指向cur的前一个结点,如果遇到val就直接删除,这里需要特殊判断一下头结点就是val的情况。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* deleteNode(ListNode* head, int val) {
        if(head->val == val) return head->next;
        ListNode *pre = head;
        ListNode *cur = head;
        while(cur!=nullptr && cur->val!=val)
        {
            pre = cur;
            cur = cur->next;
        }
        if(cur->val==val)
        {
            pre->next = cur->next;
        }
        return head;
    }
};

3.2 【双指针】剑指 Offer 22 - 链表中倒数第k个节点

https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/

  双指针left和right,先让right遍历k个结点,然后和left同时向后遍历,但right到达尾结点时left即为倒数第k个结点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode *left = head;
        ListNode *right = head;
        for(int i=0;i<k;i++)
        {
            right = right->next;
        }
        while(right)
        {
            left = left->next;
            right = right->next;
        }
        return left;
    }
};

3.3 【递归】剑指 Offer 25 - 合并两个排序的链表

https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/

  判断当前l1和l2的大小,如果l1较小,就从l1往后连接,并返回l1,同理l2。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if(l1==nullptr) return l2;
        else if(l2==nullptr) return l1;
        else if(l1->val < l2->val)
        {
            l1->next = mergeTwoLists(l1->next,l2);
            return l1;
        }
        else
        {
            l2->next = mergeTwoLists(l1,l2->next);
            return l2;
        }
    }
};

3.4 【双指针】剑指 Offer 52 - 两个链表的第一个公共节点

https://leetcode.cn/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/

  利用双指针,定义pa和pb分别指向A和B的头节点,这里判断是否相交采用了一个非常有意思的方法,假设A和B相交节点前各有skipA和skipB个节点,而相交节点数为C,pa和pb同时开始遍历,如果pa遍历完了就指向headB,同理pb,那么到最后总会遍历相同次数,对于A:skipA+C+skipB,对于B:skipB+C+skipA,如果此时的节点一样的话,说明相交,否则说明都遍历到了各自的最后节点仍不相交,返回空指针。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(headA == nullptr || headB == nullptr) return nullptr;
        ListNode *pa = headA;
        ListNode *pb = headB;
        while(pa != pb)
        {
            pa = pa == nullptr ? headB : pa->next;
            pb = pb == nullptr ? headA : pb->next;
        }
        return pa;
    }
};

3.5 【双指针】剑指 Offer 21 - 调整数组顺序使奇数位于偶数前面

https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/

  定义双指针i和j分别指向数组的第一个元素和最后一个元素,然后i往后遍历,直到找到前半部分的第一个偶数,j向前遍历,直到找到后半部分的第一个奇数,然后交换i和j所在位置的数即可,如果想不改变奇数和偶数分别在数组中的原有排列位置,这里推荐采用快慢指针来实现,让两个指针同时从头开始遍历,分别找到第一个出现的偶数和奇数交换位置。

class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        int i = 0 , j = nums.size()-1;
        while(i<j)
        {
            if(nums[i]%2==1)
            {
                i++;
                continue;
            }
            if(nums[j]%2==0)
            {
                j--;
                continue;
            }
            swap(nums[i++],nums[j--]);
        }
        return nums;
    }
};

3.6 【双指针】剑指 Offer 57 - 和为s的两个数字

https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/

  定义双指针left和right,分别指向数组的第一个元素和最后一个元素,如果两者之和大于target,则right向前移,如果两者之和小于target,则left向后移,直到两者之和等于target,输出结果。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        vector<int> ans;
        if(nums.size()==1) return ans;
        int left = 0 , right = nums.size()-1;
        while(left<right)
        {
            if(nums[left] + nums[right] < target) left++;
            else if(nums[left] + nums[right] > target) right--;
            else
            {
                ans.push_back(nums[left]);
                ans.push_back(nums[right]);
                break;
            }
        }
        return ans;
    }
};

3.7 【双指针】剑指 Offer 58 - I - 翻转单词顺序

https://leetcode.cn/problems/fan-zhuan-dan-ci-shun-xu-lcof/

  定义双指针start和end,分别表示每个单词开始和结束。这道题的难点在于,如果两个单词之间存在不止一个空格应该怎么办,这里的做法是先把整个字符串反转,然后定义了index作为全局移动的指针,表示目前字符串遍历到哪里了,同时也借助index实现以下功能:

  • 如果每个单词开始位置之前有多个空格,则去掉这些空格,使用的方法是挨个赋值代替这些空格,例如" abc"->“abcc”;
  • 当每个单词的反转结束时,下一个字符一定是空格,例如"abcc"->"cba ";
  • 当所有单词全部反转之后,会在字符串末尾留下一些空格,去掉它们,例如"cba "->“cba”。
class Solution {
public:
    string reverseWords(string s) {
        reverse(s.begin(),s.end());
        
        int index = 0;
        for(int start=0;start<s.size();start++)
        {
            if(s[start]!=' ')
            {
                //对应第二个功能
                if(index!=0) s[index++] = ' ';
                int end = start;
                //对应第一个功能
                while(end<s.size() && s[end]!=' ')
                {
                    s[index++] = s[end++];
                }
                reverse(s.begin()+index-(end-start),s.begin()+index);
                start = end;
            }
        }
        //对应第三个功能
        s.erase(s.begin()+index,s.end());
        return s;
    }
};

你可能感兴趣的:(#,力扣,leetcode,链表,算法,数据结构)