算法记录 Day8:344.反转字符串 541. 反转字符串II 剑指Offer05.替换空格 151.翻转字符串里的单词 剑指Offer58-II.左旋转字符串

LeetCode344. 反转字符串

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

1、思路

原始思路

C++ 中提供了反转相关的库函数 reverse(),可以考虑直接使用相关的库函数。

思路提升

如果直接使用库函数的话,就不会清楚反转字符串的实现原理;而且这道题目也没有出现的意义。

在反转链表中,使用了双指针的方法。那么反转字符串依然是使用双指针的方法,只不过对于字符串的反转,其实要比链表简单一些。因为字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的。

对于字符串,我们定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。

2、代码实现

class Solution {
public:
    void reverseString(vector<char>& s) {
        int left = 0;
        int right = s.size() - 1;

        while (left < right) {

            // 交换两数之数值交换法
            // char temp = s[left];
            // s[left] = s[right];
            // s[right] = temp;

            // 交换两数之位运算法
            // s[left] ^= s[right];
            // s[right] ^= s[left];
            // s[left] ^= s[right];

            // 交换两数的库函数
            swap(s[left], s[right]);
            left++;
            right--;
        }
    }
};

3、复杂度分析

时间复杂度

时间复杂度: O(N)

两个指针一个从头往后,一个从后往前,直到他们相遇,总共会把长度为N的字符数组全部遍历一遍,因此时间复杂度为O(N);

空间复杂度

空间复杂度:O(1)

原地修改的字符数组,没有占用和创建其他空间,只有两个常数级的指针。

4、思考与总结

如果在现场面试中,我们什么时候使用库函数,什么时候不要用库函数呢?

  • 如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。 毕竟面试官一定不是考察你对库函数的熟悉程度。
  • 如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。

LeetCode541. 反转字符串II

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

1、思路

原始思路

乍一看像是反转字符串,但是实际上题目中加上了一些特殊的规则,这道题实际上也不涉及到算法层面的知识,就是模拟实现规定的规则就可以了。

循环一遍数组,为了处理逻辑,每隔 2k 个字符的前 k 的字符,写了一堆逻辑代码或者再搞一个计数器,来统计 2k,再统计前 k 个字符。

思路提升

其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。

所以当需要固定规律一段一段去处理字符串的时候,要想想在 for 循环的表达式上做做文章。

2、代码实现

class Solution {
public:

    // 反转区间为左闭右闭区间
    void reverse(string& s, int start, int end) {
        while (start < end) {
            swap(s[start++], s[end--]);
        }
    }

    string reverseStr(string s, int k) {
        for (int i = 0; i < s.size(); i += (2 * k)) {
            if (i + k <= s.size()) {
                 // 1. 每隔 2k 个字符的前 k 个字符进行反转
                reverse(s, i, i + k - 1);
            } else {
                // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
                reverse(s, i, s.size() - 1);
            }
        }
        return s;    
    }
};

3、复杂度分析

时间复杂度

时间复杂度:O(N)

整个字符串长度为N,大约只有N/2的段需要处理,所以时间复杂度为O( N);

空间复杂度

空间复杂度:O(1)

原地修改的字符串,没有占用和创建其他空间。

4、思考与总结

当遇到一些边界条件不能确定的时候,直接举个栗子会让思路清晰起来。

剑指Offer 05. 替换空格

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

1、思路

原始思路

看到这道题第一反应是,将数组转换成链表,链表中可以自由插入和删除元素,插入元素后,再将链表转为数组。但是这种方法会使用额外的辅助空间。

思路提升

首先扩充数组到每个空格替换成"%20"之后的大小。然后从后向前替换空格,也就是双指针法。

2、代码实现

class Solution {
public:
    string replaceSpace(string s) {
       
        int count = 0;
        for (char ch : s) {
            if (ch == ' ') count++;
        }

        int old_index = s.size() - 1;
        s.resize(s.size() + count * 2);   // 扩充
        int new_index = s.size() - 1;

        for (; old_index >= 0; old_index--, new_index--) {
            if (s[old_index] == ' ') {
                s[new_index] = '0';
                s[new_index - 1] = '2';
                s[new_index - 2] = '%';
                new_index -= 2;
            } else {
                s[new_index] = s[old_index];
            }
        }
        return s;
    }
};

3、复杂度分析

时间复杂度

时间复杂度:O(n)

  • 首先统计字符串中空字符的数量,就有O(N)的复杂度
  • 其次加长数组之后,仍然是O(N)级别的长度,然后指针是会遍历一遍的,所以复杂度也为O(N)
  • 最后把数组转换为字符串,也是O(N)的复杂度,总体来说仍然是O(N)的时间复杂度。

空间复杂度

空间复杂度:O(1)

原地修改的字符串,没有占用和创建其他空间,只是创建几个常数级的变量。

4、思考与总结

什么要从后向前填充,从前向后填充不行么?

从前向后填充就是 O(n^2) 的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。 这么做有两个好处:

  1. 不用申请新数组。
  2. 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。

所以对于线性数据结构,填充或者删除,后序处理会高效的多。可以借这道题好好体会

5、C++语法学习

  1. 字符串和数组有什么差别? 字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,C/C++ 中的字符串如下所示:
    • 在 C 语言中,把一个字符串存入一个数组时,也把结束符 ‘\0’ 存入数组,并以此作为该字符串是否结束的标志。

    • 在 C++ 中,提供一个 string 类,string 类会提供 size 接口,可以用来判断 string 类字符串是否结束,就不用 ‘\0’ 来判断是否结束。

    • 那么 vector< char > 和 string 又有什么区别呢?

      其实在基本操作上没有区别,但是 string 提供更多的字符串处理的相关接口,例如 string 重载了+,而 vector 却没有,所以想处理字符串,定义一个 string 类型更加方便。

  2. string 类的使用
    【注意】string 中 resize() 的时间复杂度为 O(1)

151.翻转字符串里的单词

题目链接:151.翻转字符串里的单词 - 力扣

1、思路

原始思路

看到这道题,简单的思路是使用 split 库函数分隔单词,然后定义一个新的 string 字符串,最后再把单词倒序相加。但是这种方法会使用额外的辅助空间。

思路提升

我们将整个字符串都反转过来,那么单词的顺序指定是倒序了,只不过单词本身也倒序了,那么再把单词反转一下,单词不就正过来了。所以解题思路如下:

  • 移除多余空格
  • 将整个字符串反转
  • 将每个单词反转

先讨论第一步,移除多余的空格

  • 首先,字符串的前后和中间可能都有若干个多余的空格;
  • 其次,比较棘手的是字符串中间的空格,在这里,我想到的是之前在数组里面做过的移除元素这道题,在那道题里面是移除特定的元素,在这里不是非常类似吗,只是这个元素变成了空格,因此,本题可以照搬那边的思路。
  • 同样定义两个快慢指针,快指针用来遍历所有的元素,慢指针用来更新新的元素,这里要注意的是,单词之间需要保留有一个空格,这一点需要特殊处理。

第二步,将整个字符串反转

和之前做过的题目都一样,也是定义两个指针 left 和 right,一个从头,一个从尾,交换元素。

第三步,将其中的单词再反转

通过 for 循环找到一个个单词的区间,然后用第二步中定义的函数进行反转即可。

2、代码实现

class Solution {
public:
    void removeExtraSpaces(string& s) {
        int slow = 0;
        for (int fast = 0; fast < s.size(); fast++) {
            if (s[fast] != ' ') {
                if (slow != 0) s[slow++] = ' ';
                while (fast < s.size() && s[fast] != ' ') {
                    s[slow++] = s[fast++];
                }
            }
        }
        s.resize(slow);
    }

    void reverse(string& s, int start, int end) {
        while (start < end) {
            swap(s[start++], s[end--]);
        }
    }

    string reverseWords(string s) {
        removeExtraSpaces(s);
        reverse(s, 0, s.size() - 1);

        int start = 0;
        for (int i = 0; i <= s.size(); i++) {
            if (s[i] == ' ' || i == s.size()) {
                reverse(s, start,  i - 1);
                start = i + 1;
            }
        }
        return s;
    }
};

3、复杂度分析

时间复杂度

每个子函数的复杂度:

  1. removeExtraSpaces() 时间复杂度为 O(N):虽然有两个循环,但是实际上两个循环拼接起来才完整遍历一遍。
  2. reverse() 时间复杂度为 O(N);
  3. reverseWords()时间复杂度为 O(N);

总体的时间复杂度是调用的这些子函数的时间复杂度的和,因此仍然是O(N)

空间复杂度

每个子函数的复杂度:

  1. removeExtraSpaces() 空间复杂度为 O(1);
  2. reverse() 空间复杂度为 O(1);
  3. reverseWords() 空间复杂度为 O(1);

总体的空间复杂度是调用的这些子函数的时间复杂度的和,因此仍然是O(1)

4、思考与总结

这道题目基本把 刚刚做过的字符串操作 都覆盖了,不过就算知道解题思路,本题代码并不容易写,要多练一练。

剑指Offer58-II.左旋转字符串

题目链接:剑指Offer58-II.左旋转字符 - 力扣

1、思路

原始思路

又是一道反转字符串的题目,只不过是部分反转,刚看到这道题目的时候没有什么解题思路,只能直接看题解了。

思路提升

使用整体反转+局部反转 就可以实现反转单词顺序的目的。本题目的具体步骤如下:

  1. 反转区间为前n的子串
  2. 反转区间为n到末尾的子串
  3. 反转整个字符串

2、代码实现

class Solution {
public:
    void reverse(string& s, int start, int end) {
        while (start < end) {
            swap(s[start++], s[end--]);
        }
    }

    string reverseLeftWords(string s, int n) {
        reverse(s, 0, s.size() - 1);
        reverse(s, 0, s.size() -1 - n);
        reverse(s, s.size() - n, s.size() -1);
        return s;
    }
};

3、复杂度分析

时间复杂度

时间复杂度:O(N)

每个元素就操作两次反转,所以说复杂度为O(N)

空间复杂度

空间复杂度:O(1)

原地修改的字符串,没有占用和创建其他空间。

4、思考与总结

  1. 题解中的解法如果没接触过的话,应该会想不到,但是做过之后,实际上和反转字符串里的单词是一个思想;
  2. 此时我们已经反转好多次字符串了,来一起回顾一下吧。
    1. 在LeetCode344中,第一次讲到反转一个字符串应该怎么做,使用了双指针法
    2. 在541.反转字符串II中,这里开始给反转加上了一些条件,当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章;
    3. 后来在151.反转字符串中的单词,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转是一个很妙的思路
    4. 最后再讲到本题,本题则是先局部反转再 整体反转,与151类似,但是也是一种新的思路;
    5. 反转字符串一共就介绍到这里,相信大家此时对反转字符串的常见操作已经很了解了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值