代码随想录算法训练营Day08|344. 反转字符串、541. 反转字符串 II、剑指 Offer 05. 替换空格、151. 反转字符串中的单词、剑指 Offer 58 - II. 左旋转字符串

代码随想录算法训练营Day08|344. 反转字符串、541. 反转字符串 II、剑指 Offer 05. 替换空格、151. 反转字符串中的单词、剑指 Offer 58 - II. 左旋转字符串

344. 反转字符串

题目链接:344. 反转字符串

题干强调要原地修改输入数组,不能使用额外的字符数组进行赋值。这里采用双指针法,两指针分别从首尾位置进行元素互换,空间复杂度为O(1)。并且不需要考虑字符长度奇/偶的情况,因为中间元素不需要变动。

字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的。

代码如下:

class Solution {
public:
    void reverseString(vector<char>& s) {
        int len = s.size();
        int i = 0, j = len - 1;
        while (i < j) {
            char temp = s[i];
            s[i++] = s[j];
            s[j--] = temp;
        }
    }
};

其实直接使用库函数就可以实现:

reverse(s.begin(), s.end());

541. 反转字符串 II

题目链接:541. 反转字符串 II

本题在字符串反转的基础上,对字符串进行一定规则的部分反转,关键在于对遍历过程的模拟,我们逐个取2k个字符并翻转前k个字符。每次对起点遍历的间隔设置为2k即可。另外注意库函数reverse反转部分字符串内容时的使用方式:

// 用于反转整个字符串
reverse(s.begin(), s.end());

// 用于反转部分字符串,假设反转下标范围是[i, i+k-1](合计k个字符)
reverse(s.begin() + i, s.begin() + i + k);

注意reverse使用区间是左闭右开的。另外本题字符串末尾组可能不满k个,在确定反转下标前需要对末尾位数进行判断,注意只需要判断需要反转的前k位即可,代码如下:

class Solution {
public:
    string reverseStr(string s, int k) {
        for (int i = 0; i < s.size(); i += (2 * k)) {
            // 仅需要判断反转的前k位即可
            if (i + k < s.size())
                reverse(s.begin() + i, s.begin() + k + i);
            else
                reverse(s.begin() + i, s.end());
        }
        return s;
    }
};

剑指 Offer 05. 替换空格

题目链接:剑指 Offer 05. 替换空格

①新字符串拼接

如果考虑直接在给定字符串中插入空格的替换符,会对顺序存储结构的数组造成极大开销,因此考虑重新拼接一个新的字符串。

class Solution {
public:
    string replaceSpace(string s) {
        string res;
        for (int i = 0; i < s.size(); i++) {
            if (s[i] == ' ')
                res += "%20";
            else
                res += s[i];
        }
        return res;
    }
};

这样会造成一定空间的额外开销。

②原地拓充数组+双指针法

其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。为了不额外造成存储开销,考虑统计空格个数后原地进行数组拓展,会用到如下函数:

s.resize(new_size);

将数组拓充后,我们使用双指针来重新赋值:

  • 前指针指向原数组末位元素:用来将原数组的元素移动到新数组的正确位置
  • 后指针指向拓展数组的末位元素:实际赋值修改新数组的位置

当前指针匹配上空格时,则在后指针按要求插入替换字符即可。关键是需要倒序遍历,避免了从前先后填充元素导致每次添加元素都要将添加元素之后的所有元素向后移动。最后当前后指针重合,说明前面没有空格导致缩进了,即修改完成,代码如下:

class Solution {
public:
    string replaceSpace(string s) {
        int count = 0, len = s.size();
        // 先根据空格个数进行字符串拓充
        for (int i = 0; i < s.size(); i++) {
            if (s[i] == ' ')
                count++;
        }
        // 注意数组拓展为替换字符的差值
        s.resize(s.size() + 2 * count);
        // 从末尾开始使用双指针对空格进行替换
        int cur = s.size() - 1;
        int ori = len - 1;
        while (cur != ori) {
            if (s[ori] != ' ')
                s[cur--] = s[ori--];
            else {
                s[cur--] = '0';
                s[cur--] = '2';
                s[cur--] = '%';
                ori--;
            }
        }
        return s;
    }
};

151. 反转字符串中的单词

题目链接:151. 反转字符串中的单词

本题是字符串相关技术点整合的综合性问题:涉及到对冗余空格的去除;字符串首/尾空格的移除;还需要对字符串中单词进行顺序进行反转(单词以空格为序)。其中单词反转包括两个步骤:

  • 反转整体字符串
  • 反转每个单词

推荐先对空格进行处理后再进行字符串的反转,能够减少很多需要考虑的特殊情况。举例来说,对于字符串---sky--is-blue-

(以’-‘作为空格来说明)

// 先进行冗余空格和首/尾空格的去除
---sky--is-blue-  =>  sky-is-blue

// 再对整体字符串进行反转
sky-is-blue       =>  eulb-si-yks

// 再对单个单词进行反转
eulb-si-yks       =>  blue-is-sky

另外讲下去除空格的思路,这里我们将去重步骤封装后函数removeExtraSpaces来处理。

首先处理间隔中冗余的空格,这里使用到库函数string.erase(string.begin())函数:它会在删除指定位置元素的同时修改.size()大小。这意味着我们在删除空格的过程中,字符串的长度会动态变化,那么从前往后遍历循环的终止条件也会变动导致纰漏。因此需要从后往前进行遍历

void removeExtraSpaces(string &s) {
        // 先移除间隔中冗余的空格
        for (int i = s.size() - 1; i > 0; i--) {
            if (s[i] == s[i - 1] && s[i] == ' ')
                s.erase(s.begin() + i);
        }
        // 为防止内存溢出问题,对s.size()进行判断
        if (s[s.size() - 1] == ' ' && s.size() > 0)
            s.erase(s.begin() + s.size() - 1);
        if (s[0] == ' ' && s.size() > 0)
            s.erase(s.begin());
    }

注意遍历的边界是i>0,因为每次需要与前一位元素比较,防止越界。冗余空格清除后,单独再考虑首/尾出现空格的情况,使用resize()重新调整字符串的长度。完整代码如下:

class Solution {
public:
    string reverseWords(string s) {
        // 先去除首部空格
        removeExtraSpaces(s);
        int wordLeft = 0;
        for (int i = 0; i < s.size(); i++) {
            if (s[i] == ' ') {
                reverse(s.begin() + wordLeft, s.begin() + i);
                // 开始搜索下一个左边界
                while (s[i + 1] == ' ')
                    i++;
                wordLeft = i + 1;
            }
        }
        reverse(s.begin() + wordLeft, s.end());
        reverse(s.begin(), s.end());
        return s;
    }

    void removeExtraSpaces(string &s) {
        // 先移除间隔中冗余的空格
        for (int i = s.size() - 1; i > 0; i--) {
            if (s[i] == s[i - 1] && s[i] == ' ')
                s.erase(s.begin() + i);
        }
        // 为防止内存溢出问题,对s.size()进行判断
        if (s[s.size() - 1] == ' ' && s.size() > 0)
            s.erase(s.begin() + s.size() - 1);
        if (s[0] == ' ' && s.size() > 0)
            s.erase(s.begin());
    }
};

②快慢指针+模块化

这里主要对「冗余空格」清理流程进行优化,之前的流程框架是:

		void removeExtraSpaces(string &s) {
        // 先移除间隔中冗余的空格
        for (int i = s.size() - 1; i > 0; i--) {
                s.erase(s.begin() + i);
        }
    }

库函数erase()的时间复杂度为O(n),所以清除冗余空格的整体复杂度为O(n*n)。因此我们考虑使用双指针法来进行替换。这里我们使用快慢指针来实现:

  • 快指针:用来遍历数组,对冗余空格进行清除
  • 慢指针:用来进行实际的赋值,刷新数组数值,将除冗余空格外的元素向左进行挪动。

因为需要将有效元素整体向左挪动,所以需要从左往右进行遍历,最后根据慢指针位置进行数组长度的修改。

void removeExtraSpaces(string &s) {
        int fastIndex = 0, slowIndex = 0;
        // 先删除首位元素
        while (s[fastIndex] == ' ' && fastIndex < s.size())
            fastIndex++;
        // 尝试删除中间和末尾的冗余空格
        // 必须从前往后遍历,因为之后resize()会删除末尾元素
        // 记得考虑首位没有空格,前位越界的异常情况
        for (; fastIndex < s.size(); fastIndex++) {
            if (s[fastIndex] == ' ' 
                    && s[fastIndex] == s[fastIndex - 1] 
                    && fastIndex - 1 >= 0)
                continue;
            else {
                s[slowIndex++] = s[fastIndex];
            }
        }
        // 再根据末尾是否有空格来调整字符长度
        if (s[slowIndex - 1] == ' ')
            s.resize(slowIndex - 1);
        else    
            s.resize(slowIndex);
    }

完整代码如下:

class Solution {
public:
    string reverseWords(string s) {
        removeExtraSpaces(s);
        // 注意参数区间为[左闭右开]
        reverseString(s, 0, s.size());
        // 接下来逐个单词反转,此时首位一定不为空格
        int wordLeft = 0;
        for (int i = 0; i <= s.size(); i++) {
            // 因为字符串末尾为空格,所以考虑末位单词的特殊情况
            if (s[i] == ' ' || i == s.size()) {
                reverseString(s, wordLeft, i);
                wordLeft = i + 1;
            }
        }
        return s;
    }

    void removeExtraSpaces(string &s) {
        int fastIndex = 0, slowIndex = 0;
        // 先删除首位元素
        while (s[fastIndex] == ' ' && fastIndex < s.size())
            fastIndex++;
        // 尝试删除中间和末尾的冗余空格
        // 必须从前往后遍历,因为之后resize()会删除末尾元素
        // 记得考虑首位没有空格,前位越界的异常情况
        for (; fastIndex < s.size(); fastIndex++) {
            if (s[fastIndex] == ' ' 
                    && s[fastIndex] == s[fastIndex - 1] 
                    && fastIndex - 1 >= 0)
                continue;
            else {
                s[slowIndex++] = s[fastIndex];
            }
        }
        // 再根据末尾是否有空格来调整字符长度
        if (s[slowIndex - 1] == ' ')
            s.resize(slowIndex - 1);
        else    
            s.resize(slowIndex);
    }

    // 输入的区间为[左闭右闭]区间
    void reverseString(string &s, int begin, int len) {
        int end = len - 1;
        for (; begin < end; begin++, end--) {
            char temp  = s[begin];
            s[begin] = s[end];
            s[end] = temp;
        }
    }

    
};

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

题目链接:剑指 Offer 58 - II. 左旋转字符串

题干已知给出的前k位一定为有效值,不需要进行异常处理。


①扩展字符串

考虑对原数组进行拓展,进行数组变换过程类似位运算,字符变换流程如下:

// 给定字符串 左旋前2位
"abcdefg"

//拓充k位,并将前k位赋值到末尾
"abcdefg(ab)"

// 将字符串整体向前平移k位(快慢指针)
"cdefgab(ab)"

// 再将字符串缩短为原来的长度
"cdefgab"

代码如下:

class Solution {
public:
    string reverseLeftWords(string s, int n) {
        int oldSize = s.size();
         s.resize(s.size() + n);
         for(int i = 0, j = oldSize; j < s.size(); i++, j++) {
             s[j] = s[i];
         }
         // 双指针进行移位
         int fastIndex = 0, slowIndex = n;
         while(slowIndex < s.size()) {
             s[fastIndex++] = s[slowIndex++];
         }
         s.resize(oldSize);
         return s;
    }
};

②不申请额外空间(⭐️)

不需要额外对字符串长度进行更改,流程如下:

// 给定字符串 左旋前k(k=2)位
"abcdefg"

// 以第k位为边界将字符串划分为两段,分别进行反转
"(ba)(gfedc)"

// 将字符串整体反转
"(cdefg)(ab)"

注意需要优先划分字符串进行反转,因为如果先反转整体字符串的话,左旋的参数位置坐标会发生变化,代码如下:

class Solution {
public:
    string reverseLeftWords(string s, int n) {
        // 局部翻转
        reverseString(s, 0, n);
        cout << s << endl;
        reverseString(s, n, s.size());
        cout << s << endl;
        // 全局翻转
        reverseString(s, 0, s.size());
        return s;
    }

    void reverseString(string &s, int begin, int len) {
        int end = len - 1;
        while (begin < end) {
            char temp = s[begin];
            s[begin++] = s[end];
            s[end--] = temp;
        }
    }
};
posted @ 2022-11-23 22:41  脱线森林`  阅读(1863)  评论(0编辑  收藏  举报