代码随想录刷题总结:数组与字符串

本文深入探讨了二分查找算法的细节,包括C++中std::upper_bound和std::lower_bound函数的实现,并分析了闭区间与左闭右开区间在二分查找中的映射关系。同时,介绍了双指针法在数组处理中的应用,如移除特定元素、原地重排数组以及字符串处理等。文章还涵盖了滑动窗口在解决连续子数组问题上的运用,以及模拟算法在矩阵打印和字符串处理中的解决方案。

一、二分法

704. 二分查找

细节注意:mid=beg+end−beg2mid=beg+\frac{end-beg}{2}mid=beg+2endbeg,如此计算等价于 mid=beg+end2mid=\frac{beg+end}{2}mid=2beg+end,但后者可能导致溢出。

C++ std::upper_bound 函数实现

/* Returns an iterator pointing to the first element
 * in the range [first, last) which compares greater than val.
 */
template <class ForwardIterator, class T>
ForwardIterator upper_bound(ForwardIterator first, ForwardIterator last, const T& val) {
    ForwardIterator it;
    iterator_traits<ForwardIterator>::difference_type count, step;
    count = std::distance(first, last); // count is always be (last - first) 
    while (count > 0) {
        it = first;
        step = count / 2; // step = (last - first) / 2
        std::advance(it, step); // it = first + (last - first) / 2
        if (!(val < *it)) {
            first = ++it; // choose the right range (it, last) 
            count -= step + 1;
        }
        else
            count = step; // choose the left range [first, it)
    }
    return first;
}

C++ lower_bound 函数实现

/* Returns an iterator pointing to the first element
 * in the range [first, last) which does not compare less than val.
 */
template <class ForwardIterator, class T>
ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const T& val) {
    ForwardIterator it;
    iterator_traits<ForwardIterator>::difference_type count, step;
    count = std::distance(first, last);
    while (count > 0) {
        it = first;
        step = count / 2;
        std::advance(it, step);
        if (*it < val) {
            first = ++it;
            count -= step + 1;
        }
        else
            count = step;
    }
    return first;
}

二分法中闭区间和左闭右开区间之间的映射关系

mid=beg+end2mid=\frac{beg + end}{2}mid=2beg+end 这一运算,当区间长度为奇数时,两种区间都是对应中间位置,当区间长度为偶数时,左闭右开区间对应中间偏右的位置,闭区间对应中间偏左的位置

左闭右开区间闭区间
偶数长度区间偏左中值beg+end−beg−12beg+\frac{end-beg-1}{2}beg+2endbeg1beg+end−beg2beg+\frac{end-beg}{2}beg+2endbeg前者当区间长度为奇数时会偏左
偶数长度区间偏右中值beg+end−beg2beg+\frac{end-beg}{2}beg+2endbegbeg+end−beg+12beg+\frac{end-beg+1}{2}beg+2endbeg+1后者当区间长度为奇数时会偏右
奇数长度区间中值beg+end−beg2beg+\frac{end-beg}{2}beg+2endbegbeg+end−beg2beg+\frac{end-beg}{2}beg+2endbeg-
区间长度不为 0beg<endbeg<endbeg<endbeg≤endbeg\le endbegend前者若条件允许建议使用 ≠\ne=
区间长度不为 1beg<end−1beg<end-1beg<end1beg<endbeg<endbeg<end-
选取不含 mid 的左区间end=midend=midend=midend=mid−1end=mid-1end=mid1后者请注意 mid=0mid=0mid=0 的情况
选取不含 mid 的右区间beg=mid+1beg=mid+1beg=mid+1beg=mid+1beg=mid+1beg=mid+1-

35. 二分查找,若找不到返回其插入后的位置

细节注意:检查最后区间长度收敛到 1 时该元素与被查找元素的相对大小。

34. 实现 std::equal_range 闭区间版

细节注意:先求的边界值可以用来初始化后求的边界值的搜索范围,C++ 的 std::equal_range 的实现也用了同种方法

69. 求 xxx 的平方根(保留整数)

本题可直接借鉴 std::upper_bound 的思想,搜索第一个平方值大于 xxx 的数,最后返回时将该数减 1 即可
本题亦有利用指数和对数的解法以及牛顿迭代法

367. 判断一个整数是否恰好是某个整数的平方(完全平方数)

二分查找即可,注意整数溢出
本题亦可使用库函数或使用牛顿迭代法辅助判断

二、双指针

27. 移除数组中等于某个值的元素(原地重排数组)

  1. 双指针法:模拟人类思维的方法(可保证删除后得到的数组相对顺序不变)
  2. 相向双指针法:类似快排中的 partialize 方法,将数组靠后部分的不等于给定值的元素与数组靠前部分的等于给定值的元素进行交换,直到前后指针相遇即可,如此两个指针总共加一起只遍历一遍数组,理论上比前面的双指针法更快,但可能改变相对顺序

26. 实现 std::unique

双指针法,可参考 C++ STL 源码

283. 把 0 元素全部移动到数组后面,非零元素相对顺序不变(原地重排数组)

参照 27 双指针法

844. 比较两个含退格的字符串是否显示一致(空间复杂度 O(1)O(1)O(1)

  1. 参照 27 双指针法,原地删除对应的字符再进行比较
  2. 逆序遍历并进行比较。遇到退格符时,继续进行逆序遍历但要对跳过的字符进行计数,初始值为 1,再次遇到退格符时计数加 1,否则计数减 1,直到计数回到 0 再进行比较。须注意如此计数可能导致下标减到负值,对负值下标需要进行特别处理。

977. 对非递减数组的每个元素计算其平方值得到一个新数组,要求新数组为非递减(时间复杂度 O(n)O(n)O(n)

将原数组逐个计算其平方后可以得到一个 V 形数组,设置从两边开始的双指针,使用类似归并排序的 merge 方法对两个递减数组进行反向合并即可,注意每次取较大值放入结果数组,循环结束条件为两个指针相遇。

344. 反转字符串(实现 std::reverse

使用双指针进行元素交换即可。

541. 每隔 k 个元素反转字符串的 k 个元素

每隔 2k 个元素对当前块中的字符作对应的处理即可,注意边界条件。

151. 颠倒字符串中的单词顺序(字符串中只有字母数字字符和空格)

  • Python 可以直接使用 " ".join(reverse(s.split()))
  • 对于 C++,可以先反转整个字符串,令 idx(s 的插入位置)为 0,start 表示待插入单词的开始位置,初始化为 0 之后执行如下的循环
    1. 右移 start 直至其到达字符串末尾或不为空格,若 start 到达末尾则结束循环。否则,若 idx 不为 0,则说明 idx 前面有单词,令 s[idx] 为空格,再令 idx 自增 1,准备向 idx 插入新的单词。
    2. 设 start 标记的单词的尾后位置为 end,将子串 [start, end) 移动到 [idx, idx + (end - start)) 这个区间,并令 idx 移动到子串移动后的尾后位置 idx + (end - start)。
    3. 此时移动后的子串所在区间为 [idx - (end - start), idx),将这一部分反转。
    4. 令 start 为 end,返回 1。
      循环执行结束后,idx 若还未达到 s.length(),则说明后面的字符已经被添加完毕了,直接删除即可。
  • 使用栈或双端队列保存每个单词,再将单词反向输出即可,注意添加空格和边界条件。

28. 实现 std::string::find

  • 暴力破解,对 haystackhaystackhaystack 中每一个属于 [0,haystack.length−needle.length)[0,haystack.length-needle.length)[0,haystack.lengthneedle.length) 的下标进行检查

  • KMP 算法

    前缀 prefixprefixprefix:包含首字符的连续子串
    后缀 suffixsuffixsuffix:包含尾字符的连续子串
    最长相等前后缀长度 nnn:对于字符串 sss,求最大的 nnn,使得 s[0,n)=s[s.length−n,s.length)s[0,n) = s[s.length-n,s.length)s[0,n)=s[s.lengthn,s.length),注意不是对称而是相等

    1. 计算 needleneedleneedle 的每个形如 needle[0,i]needle[0,i]needle[0,i] 的子串的最长相等前后缀长度,记为 next[i]next[i]next[i],可见 nextnextnext 为长度为 needle.lengthneedle.lengthneedle.length 的数组。特别地,令 next[0]:=−1next[0]:=-1next[0]:=1.
    2. haystackhaystackhaystack 的初始下标 strIdxstrIdxstrIdx 为 0,needleneedleneedle 的初始下标 modIdxmodIdxmodIdx−1-11.
    3. 检查 strIdxstrIdxstrIdx 是否越界,若越界则返回 −1-11.
    4. haystack[strIdx]needle[modIdx+1]haystack[strIdx]needle[modIdx + 1]haystack[strIdx]needle[modIdx+1],则说明这两项匹配,将 strIdxstrIdxstrIdxmodIdxmodIdxmodIdx 均自增 111,否则跳转到 6.
    5. 若此时 modIdxmodIdxmodIdx 已经到达 needleneedleneedle 的末尾,则返回匹配结果 strIdx−needle.length+1strIdx-needle.length+1strIdxneedle.length+1,否则令 strIdxstrIdxstrIdx 自增 111 并跳转到 3.
    6. 不断地令 modIdx:=next[modIdx]modIdx:=next[modIdx]modIdx:=next[modIdx],直到 modIdxmodIdxmodIdx−1-11 或满足 haystack[strIdx]=needle[modIdx+1]haystack[strIdx]=needle[modIdx + 1]haystack[strIdx]=needle[modIdx+1],跳转到 4.

    nextnextnext 数组的计算方法:

    1. 首先令 next[0]:=−1next[0]:=-1next[0]:=1prefixIdx:=−1prefixIdx:=-1prefixIdx:=1suffixIdx:=1suffixIdx:=1suffixIdx:=1,其中 suffixIdxsuffixIdxsuffixIdx 表示当前处理的后缀末尾元素下标,prefixIdxprefixIdxprefixIdx 表示当前计算的 next[suffixIdx]next[suffixIdx]next[suffixIdx] 对应的最长相等前后缀的前缀的末尾元素下标,可见当最终计算出的 prefixIdxprefixIdxprefixIdx−1-11 时,说明 suffixIdxsuffixIdxsuffixIdx 对应的最长相等前后缀均为空。
    2. 检查 suffixIdxsuffixIdxsuffixIdx 是否越界,若越界则 nextnextnext 数组已经计算完毕,退出循环。
    3. needle[suffixIdx]=needle[prefixIdx+1]needle[suffixIdx]=needle[prefixIdx + 1]needle[suffixIdx]=needle[prefixIdx+1] 则说明最长相等前后缀可以扩展,令 prefixIdxprefixIdxprefixIdx 自增 1,之后令 next[suffixIdx]:=prefixIdxnext[suffixIdx]:=prefixIdxnext[suffixIdx]:=prefixIdx,跳转到 2.
    4. 不断地令 prefixIdx:=next[prefixIdx]prefixIdx:=next[prefixIdx]prefixIdx:=next[prefixIdx],直到 prefixIdxprefixIdxprefixIdx−1-11 或满足 needle[suffixIdx]=needle[prefixIdx+1]needle[suffixIdx]=needle[prefixIdx + 1]needle[suffixIdx]=needle[prefixIdx+1],跳转到 3.
class Solution {
public:
    /**
     * e.g. haystack = "abeababeabf"; needle = "abeabf";
     * next = {-1, 0, 0, 1, 0, 0};
     * The values for each time the "match" for-loop begin:
     * strIdx:  0   1   2   3   4   5   6   7   8   9   10
     * modIdx:  -1  0   1   2   3   4   0   1   2   3   4
     * when strIdx = 5 and modIdx = 4:
     * 1. modIdx rollback to -1
     * 2. ++modIdx
     */
    int strStr(string haystack, string needle) {
        int modLen = needle.size();
        int strLen = haystack.size();

        vector<int> next(modLen, 0);
        next[0] = -1;
        int prefixIdx = -1; // stand for the empty prefix

        // calculate next[1:modLen]
        for (int suffixIdx = 1; suffixIdx < modLen; ++suffixIdx) {
            // rollback to the longest prefix for prefix + needle[prefixIdx + 1] == suffix
            // prefixIdx may become -1
            while (prefixIdx >= 0 && needle[suffixIdx] != needle[prefixIdx + 1]) {
                prefixIdx = next[prefixIdx];
            }
            // the length of prefix is able to be appended
            if (needle[suffixIdx] == needle[prefixIdx + 1]) {
                ++prefixIdx;
            }
            next[suffixIdx] = prefixIdx; // next[suffixIdx] maybe -1
        }

        // match
        int modIdx = -1; // stand for the empty mod prefix
        for (int strIdx = 0; strIdx < strLen; ++strIdx) {
            // rollback to the longest modIdx for needle[0:modIdx + 2] == haystack[0:strIdx + 1]
            // modIdx maybe -1
            while (modIdx >= 0 && haystack[strIdx] != needle[modIdx + 1]) {
                modIdx = next[modIdx];
            }
            // modIdx is able to be increased
            if (haystack[strIdx] == needle[modIdx + 1]) {
                ++modIdx;
            }
            // success
            if (modIdx == modLen - 1) {
                return strIdx - modLen + 1;
            }
        }
        return -1;
    }
};

459. 检查字符串 s 是否由多个相同的连续子串相加而成

  • t=(s+s)[1:2∗s.length−1]t=(s+s)[1:2*s.length-1]t=(s+s)[1:2s.length1],若能在 t 中找到 s,则说明 s 由多个子串 s’ 组成,即 s=k∗s′s=k*s's=ks
class Solution {
public:
    bool repeatedSubstringPattern(string s) {
        return (s + s).find(s, 1) != s.size();
    }
};
  • KMP 算法 1:使用 KMP 算法检查模式 s 和上个解法中的待测字符串 t 是否匹配。
  • KMP 算法 2:先计算 next 数组,之后检查字符串长度是否可以被(字符串长度 - 最长相等前后缀长度)整除即可。
  • 统计每个字符的出现次数,计算出现次数非零的各个字符的出现次数的最大公约数 gcd,若 gcd 不为 1,则将字符串分为 gcd 块,检查每个块是否相等即可。

三、滑动窗口

209. 寻找和大于等于正整数 xxx 的最短连续子数组(数组中均为正整数)

维护一个滑动窗口,若其中所有数的和小于 xxx,则滑动右边界,否则滑动左边界,滑动时要刷新窗口中的数字和,记录滑动过程中的最小的窗口长度即可。须注意的细节是,若右边界已达到数组尾端而总和仍然小于 xxx 时可以直接停止计算

904. 寻找元素种类数小于等于 2 的最长连续子数组

维护一个滑动窗口,用一个哈希表维护当前窗口中的每个元素的个数,视情况对数组进行滑动,记录最大的符合条件的窗口,须注意的细节是,若右边界已达到数组尾端可以直接停止计算

76. 给定字符串 sssttt,寻找 sss 的包含 ttt 中所有字符的最小连续子串

维护一个滑动窗口 [b,e)[b, e)[b,e)。建立两个哈希表 hshshshththt,其中 hshshs 用于存储当前滑动窗口中每个字符出现的次数,hththt 用于存储 ttt 中字符出现的次数。建立一个变量 cntcntcnt 表示当前滑动窗口对应的连续子串中已经匹配了多少个 ttt 中的字符,对于滑动窗口中的任意字符 ccc,最多匹配 ht(c)ht(c)ht(c) 次。
于是,我们在循环中每次将 eee 右移,每次根据并入的字符更新 cntcntcnt,之后不断右移 bbb,直到当前右移操作会导致 hs(s(b))<ht(s(b))hs(s(b)) < ht(s(b))hs(s(b))<ht(s(b)) 为止,若此时 cnt=s.lengthcnt = s.lengthcnt=s.length,计算当前窗口长度,若当前窗口比原来的窗口更短(假定窗口长度初始为无穷大),则记录当前窗口。

四、模拟

54. 剑指 Offer 29. 将矩阵按顺时针螺旋的顺序打印到一个数组中

转圈遍历,每转向一次便缩小范围,比如到达右上角准备向下遍历之前,把遍历上界自增 1 即可,注意细节

59. 将给定的数组转化成顺时针螺旋的正方形矩阵

与上一题类似

剑指 Offer 05. 将字符串中的空格替换为 “%20”

可以先统计空格数量预先分配空间,也可以直接把 string 当作动态数组。

剑指 Offer 58 - II. 将字符串循环左移 k 次

  • 新建一个字符串然后分片拷贝即可
  • 先反转前 k 个字符,再反转剩余的字符,最后反转整个字符串
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值